0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【2Dマップ生成】Unityでso_longを作る

Posted at

#自己紹介
こんにちは、42tokyo Advent Calendar 2021 の9日目を担当する、在校生のtkanzakiです。
マップが "so long" でも動く2Dゲームを作るという課題があったので、Unityで作ってみました。

#やりたいこと
巨大なマップを読み取って一定範囲内のマップを生成する
いっぺんにオブジェクトを生成すると時間がかかってしまうのでプレイヤーの回りだけ生成されるようにする
so_long_unity - SampleScene - PC, Mac & Linux Standalone - Unity 2020.3.21f1 Personal_ DX11 2021-12-05 10-55-16_6.gif

#実装手順
1.マップをテキストファイルから読み取る
2.マップのサイズに合わせて背景を出す
3.プレイヤーを出す
4.プレイヤーを動かす
5.プレイヤーの一定範囲にゲームオブジェクトを生成する
so_long_unity7.png
BG3.png

上の2つの画像を使うのでダウンロードしてください
画像は自作したものなのでフリー素材として使っていただいて大丈夫です
Unityのバージョンは何でもいいです
UnityHubから2Dゲームのテンプレートでプロジェクトを新規作成してください

##1.マップをテキストファイルから読み取る
・MapCreater.cs:マップを生成する
・BlockData.cs:マップの1文字(ブロック)に対応する要素
・BlockCharacter.cs:ブロックの文字の列挙型
・map.txt:マップのテキストファイル
この4つのファイルをAssets下にメニューバーから Assets > Create で作ってください
テキストファイルなどはドラッグアンドドロップすればインポートできます
スクリーンショット (57).png

コピペで使えます

MapCreater.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class MapCreater : MonoBehaviour
{
    [SerializeField] private string mapFilePath; // マップファイルのパス

    private List<List<BlockData>> mapBlocks = new List<List<BlockData>>(); // BlockDataクラスの二次元リスト

    void Start()
    {
        // マップを読み込み
        ReadMap(mapFilePath);
    }

    private void ReadMap(string filePath)
    {
        try
        {
            using (var stream = new StreamReader(filePath))
            {
                InitBlocks(stream);
                // マップの情報をコンソールに出力
                PutDebugLog();
            }
        }
        catch (System.Exception e)
        {
            Debug.Log($"Error!{e}");
        }
    }

    // マップデータを読み込んでブロックリストに格納
    private void InitBlocks(StreamReader stream)
    {
        var count = 0;
        var line = stream.ReadLine();

        while (line != null)
        {
            mapBlocks.Add(new List<BlockData>());
            InitMapLine(line, count);
            line = stream.ReadLine();
            count++;
        }
    }

    // 一行分の読み込んだマップデータをブロックリストに格納
    private void InitMapLine(string line, int index)
    {
        var count = 0;

        foreach (var item in line)
        {
            var character = (BlockCharacter)item;
            var block = new BlockData(character);
            mapBlocks[index].Add(block);
            count++;
        }
    }

    // コンソール出力
    private void PutDebugLog()
    {
        foreach (var line in mapBlocks)
        {
            foreach (var block in line)
            {
                Debug.Log($"block char is {block.GetCharacter()}");
            }
        }
    }
}
BlockData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlockData
{
    private BlockCharacter character;

    public BlockData(BlockCharacter character)
    {
        this.character = character;
    }

    public BlockCharacter GetCharacter()
    {
        return (character);
    }
}
BlockCharacter
public enum BlockCharacter
{
    Empty = '0',
    Wall = '1',
    Player = 'P'
}
map.txt
11111
10P01
11111

Hierarchy から Create Empty で MapCreater を作成
スクリーンショット (58).png
AddComponent で MapCreater を追加します
Map File Path にマップファイルのパスを入力します
スクリーンショット (59).png
Unity Editor 上部の実行ボタンを押してしてコンソールを確認します
スクリーンショット (101).png
スクリーンショット (61).png
map.txt と同じ数出力されていれば成功です

##2.マップのサイズに合わせて背景を出す
MapCreater に CreateBG を追加しました
マップを読み込んだ時に縦と横の拡大率が決まります

MapCreater.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class MapCreater : MonoBehaviour
{
    [SerializeField] private string mapFilePath; // マップファイルのパス

    [SerializeField] private GameObject bG;     // 背景のゲームオブジェクト
    [SerializeField] private int bGImageWidth;  // 背景画像の幅
    [SerializeField] private int bGImageHeight; // 背景画像の高さ
    private float bgScaleX; // 背景画像のXの拡大率
    private float bgScaleY; // 背景画像のYの拡大率

    private List<List<BlockData>> mapBlocks = new List<List<BlockData>>(); // BlockDataクラスの二次元リスト
    private int mapBlocksWidth;  // マップ内の横のブロックの数
    private int mapBlocksHeight; // マップ内の縦のブロックの数

    // Start is called before the first frame update
    void Start()
    {
        // マップを読み込み
        ReadMap(mapFilePath);

        // 背景を作成
        CreateBg();
    }

    // 省略

    // マップデータを読み込んでブロックリストに格納
    private void InitBlocks(StreamReader stream)
    {
        var count = 0;
        var line = stream.ReadLine();

        while (line != null)
        {
            mapBlocks.Add(new List<BlockData>());
            InitMapLine(line, count);
            line = stream.ReadLine();
            count++;
        }

        // マップの幅をセット
        this.mapBlocksHeight = count;

        // 背景の画像に対する大きさをセット
        this.bgScaleY = (float)this.mapBlocksHeight / (float)this.bGImageHeight;
    }

    // 一行分の読み込んだマップデータをブロックリストに格納
    private void InitMapLine(string line, int index)
    {
        var count = 0;

        foreach (var item in line)
        {
            var character = (BlockCharacter)item;
            var block = new BlockData(character);
            mapBlocks[index].Add(block);
            count++;
        }

        // マップの幅をセット
        this.mapBlocksWidth = count;

        // 背景の幅をセット
        this.bgScaleX = (float)this.mapBlocksWidth / (float)this.bGImageWidth;
    }

    private void CreateBg()
    {
        var bGObject = Instantiate(this.bG);
        bGObject.transform.position = new Vector3(0.5f, 1f, 0f);
        bGObject.transform.localScale = new Vector3(this.bgScaleX, this.bgScaleY, 1);
    }
}

背景画像をAssets下に置いたら、
Pixels Per Unit を 1
Pivot を Top Left
に変更して、Apply してください
Pivot を左上にするのはマップを読み取るのが左上からだからです
スクリーンショット (118).png
スクリーンショット (120).png

Assets から Hierarchy にドラッグアンドドロップするとプレハブ化されるので、プレハブ化した後にAssetsに戻しましょう
スクリーンショット (122).png

MapCreaterにプレハブを割り当てて幅と高さを入力します
スクリーンショット (124).png

実行してマップと同じサイズの背景画像が出たら成功です
スクリーンショット (126).png

##3.プレイヤーを出す
スプライト画像をAssets下に置いたら、
Sprite Mode をMultiple
Pixels Per Unit を 128
に変更して、Apply してください
スクリーンショット (113).png
そして、Sprite Editor を開き、
Type を Grid By Cell Size
Pixel Size を 128 x 128
Pivot を Bottom
で Slice して Apply してください
スクリーンショット (115).png

Sprite Editor を閉じたら、BG と同じ要領で、
sprite_0 を Hierarchy にドラッグアンドドロップしてプレハブ化してください
名前はわかりやすいように Player にリネームしてください
スクリーンショット (128).png

Player の Sprite Renderer の Order in Layer を 1 にして、BG より上に描画するようにします
スクリーンショット (130).png

MapCreater に CreatePlayer を追加しました

MapCreater.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class MapCreater : MonoBehaviour
{
    [SerializeField] private string mapFilePath; // マップファイルのパス

    [SerializeField] private GameObject bG;     // 背景のゲームオブジェクト
    [SerializeField] private int bGImageWidth;  // 背景画像の幅
    [SerializeField] private int bGImageHeight; // 背景画像の高さ
    private float bgScaleX; // 背景画像のXの拡大率
    private float bgScaleY; // 背景画像のYの拡大率

    [SerializeField] private GameObject player; // プレイヤーのゲームオブジェクト
    private int playerStartPosX; // プレイヤーのXの開始位置
    private int playerStartPosY; // プレイヤーのYの開始位置

    private List<List<BlockData>> mapBlocks = new List<List<BlockData>>(); // BlockDataクラスの二次元リスト
    private int mapBlocksWidth;  // マップ内の横のブロックの数
    private int mapBlocksHeight; // マップ内の縦のブロックの数

    // Start is called before the first frame update
    void Start()
    {
        // マップを読み込み
        ReadMap(mapFilePath);

        // 背景を作成
        CreateBg();

        // プレイヤーを作成
        CreatePlayer();
    }

    // 省略

    // 一行分の読み込んだマップデータをブロックリストに格納
    private void InitMapLine(string line, int index)
    {
        var count = 0;

        foreach (var item in line)
        {
            var character = (BlockCharacter)item;
            var block = new BlockData(character);
            mapBlocks[index].Add(block);
            count++;

            // プレイヤーの開始ポジションをセット
            if (character == BlockCharacter.Player)
            {
                playerStartPosX = count;
                playerStartPosY = (-1) * index;
            }
        }

        // マップの幅をセット
        this.mapBlocksWidth = count;

        // 背景の幅をセット
        this.bgScaleX = (float)this.mapBlocksWidth / (float)this.bGImageWidth;
    }

    // 省略

    private void CreatePlayer()
    {
        var player = Instantiate(this.player);
        player.transform.position = new Vector3(this.playerStartPosX, this.playerStartPosY, 1);
    }
}

MapCreater に Player を入れる
スクリーンショット (84).png
実行してマップと同じ位置にプレイヤーが出たら成功です
スクリーンショット (132).png

##4.プレイヤーを動かす
まずは、Player を動かすので Player.cs を作り Player に Add Component しましょう

Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] private Animator animator;
    [SerializeField] private float moveSpeed;

    private MapCreater mapCreater;

    void Start()
    {
        mapCreater = GameObject.FindWithTag("MapCreater").GetComponent<MapCreater>();
    }

    void Update()
    {
        // アニメーション切り替え
        AnimationChange();

        // キャラクター移動
        Move();
    }

    private void Move()
    {
        var xPos = this.transform.position.x + Input.GetAxis("Horizontal") * Time.deltaTime * moveSpeed;
        var yPos = this.transform.position.y + Input.GetAxis("Vertical") * Time.deltaTime * moveSpeed;

        transform.position = new Vector3(xPos, yPos, 0f);
    }

    private void AnimationChange()
    {
        // 反転
        SetDirection();
    }

    private void SetDirection()
    {
        var horizon = Input.GetAxis("Horizontal");
        var scale = this.transform.localScale;

        if (horizon > 0.1f)
        {
            scale.x = 1;
        }
        else if (horizon < -0.1f)
        {
            scale.x = -1;
        }
        else
        {
            return;
        }
        transform.localScale = scale;
    }
}

Move Speed を 3 くらいにします
スクリーンショット (89).png
Add Tag から MapCreater というタグを作って MapCreater に付けます
こうすることで Player から FindWithTag で参照できるようになります
スクリーンショット (93).png
Player を Open Prefab してカメラを付けることでプレイヤーに追従してくれます
スクリーンショット (91).png

camera の transform.position.z を -4 にして手前から写るようにします
スクリーンショット (135).png

so_long_unity - SampleScene - PC, Mac & Linux Standalone - Unity 2020.3.21f1 Personal_ DX11 2021-12-05 10-55-16_7.gif

##5.プレイヤーの一定範囲にオブジェクトを生成する

MapCreater に CreateArround と DeleteArround を追加し、Player から呼び出します

MapCreater.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class MapCreater : MonoBehaviour
{
    [SerializeField] private string mapFilePath; // マップファイルのパス

    [SerializeField] private GameObject bG;     // 背景のゲームオブジェクト
    [SerializeField] private int bGImageWidth;  // 背景画像の幅
    [SerializeField] private int bGImageHeight; // 背景画像の高さ
    private float bgScaleX; // 背景画像のXの拡大率
    private float bgScaleY; // 背景画像のYの拡大率

    [SerializeField] private GameObject player; // プレイヤーのゲームオブジェクト
    private int playerStartPosX; // プレイヤーのXの開始位置
    private int playerStartPosY; // プレイヤーのYの開始位置

    private List<List<BlockData>> mapBlocks = new List<List<BlockData>>(); // BlockDataクラスの二次元リスト
    private int mapBlocksWidth;  // マップ内の横のブロックの数
    private int mapBlocksHeight; // マップ内の縦のブロックの数
    [SerializeField] private float enableRange;  // ブロックの有効範囲
    [SerializeField] private float disableRange; // ブロックを無効範囲

    [SerializeField] private GameObject wall; // 壁のゲームオブジェクト

    // 省略

    public void CreateAround(float posX, float posY)
    {
        posY = Mathf.Abs(posY);
        var startX = (posX - this.enableRange) > 0f ? (posX - this.enableRange) : 0f;
        var endX = (posX + this.enableRange) < (float)this.mapBlocksWidth ? (posX + this.enableRange) : (float)this.mapBlocksWidth;
        var startY = (posY - this.enableRange) > 0f ? (posY - this.enableRange) : 0f;
        var endY = (posY + this.enableRange) < (float)this.mapBlocksHeight ? (posY + this.enableRange) : (float)this.mapBlocksHeight;

        for (int i = (int)startY; i < (int)endY; i++)
        {
            for (int j = (int)startX; j < (int)endX; j++)
            {
                if (mapBlocks[i][j].IsAlreadyRead == true)
                {
                    continue;
                }

                if (mapBlocks[i][j].GetCharacter() == BlockCharacter.Wall)
                {
                    Debug.Log($"{j}, {i}, {mapBlocks[i][j].GetCharacter()} is created.");
                    mapBlocks[i][j].GameObject = Instantiate(this.wall);
                    mapBlocks[i][j].GameObject.transform.position = new Vector3(j + 1, -i, 0);
                }
                mapBlocks[i][j].IsAlreadyRead = true;
            }
        }
    }

    public void DeleteAround(float posX, float posY)
    {
        posY = Mathf.Abs(posY);
        var startX = (posX - this.disableRange) > 0f ? (posX - this.disableRange) : 0f;
        var endX = (posX + this.disableRange) < (float)this.mapBlocksWidth ? (posX + this.disableRange) : (float)this.mapBlocksWidth;
        var startY = (posY - this.disableRange) > 0f ? (posY - this.disableRange) : 0f;
        var endY = (posY + this.disableRange) < (float)this.mapBlocksHeight ? (posY + this.disableRange) : (float)this.mapBlocksHeight;

        for (int i = 0; i < this.mapBlocksHeight; i++)
        {
            for (int j = 0; j < mapBlocksWidth; j++)
            {
                // 一定範囲内なら削除しない
                if ((i >= startY && j >= startX && i <= endY && j <= endX) || mapBlocks[i][j].IsAlreadyRead == false)
                {
                    continue;
                }
                if (mapBlocks[i][j].GetCharacter() == BlockCharacter.Wall)
                {
                    Debug.Log($"{j}, {i}, {mapBlocks[i][j].GetCharacter()} is deleted.");
                    Destroy(mapBlocks[i][j].GameObject);
                }
                mapBlocks[i][j].IsAlreadyRead = false;
            }
        }
    }
}
BlockData.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlockData
{
    private BlockCharacter character;

    public bool IsAlreadyRead { get; set; }
    public GameObject GameObject { get; set; }

    public BlockData(BlockCharacter character)
    {
        this.character = character;
        IsAlreadyRead = false;
    }

    public BlockCharacter GetCharacter()
    {
        return (character);
    }
}
Player.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] private float moveSpeed;

    private MapCreater mapCreater;

    // 省略

    void FixedUpdate()
    {
        // プレイヤーの一定範囲のマップを読み込む
        mapCreater.CreateAround(this.transform.position.x, this.transform.position.y);

        // プレイヤーの一定範囲外のマップを消す
        mapCreater.DeleteAround(this.transform.position.x, this.transform.position.y);
    }

    // 省略

}

わかりやすいようにマップを大きくします

map.txt
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
1000000P0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101101
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111

sprite_9 を Hierarchy にドラッグアンドドロップして Wall のプレハブを作ります
スクリーンショット (137).png

Sprite Renderer の Order in Layer も 1 にします
スクリーンショット (139).png

MapCreater の
Enable Range に 10 を入力
Disable Range に 15 を入力
Wall にプレハブを割り当て
スクリーンショット (99).png
プレイヤーにあたり判定を付与します
Rigidbody 2D を Add Component
Gravity Scale を 0
Constrains の Freeze Rotation の Z を有効化
Box Collider 2D を Add Component
スクリーンショット (105).png
壁も同様にあたり判定を付与します
壁は動かないので Freeze Position の X, Y を 有効にします
スクリーンショット (104).png
見やすいように Unity Editor のレイアウトを 2 by 3 に変更して実行すると
so_long_unity - SampleScene - PC, Mac & Linux Standalone - Unity 2020.3.21f1 Personal_ DX11 2021-12-05 10-55-16_8.gif

うまくいきました!

長すぎると見づらいのでアニメーションは省略しました
プレイヤーにアニメーションを付けるスクリプトはGithubで公開しときます
Animator や Animation は自分で作らないと動かないので調べてみてください

敵や、ゴールを実装するとゲームらしくなるのでよかったら自分なりに拡張してみてください

#終わりに
42ではminilibXというライブラリを使ってC言語で作るのですが、めちゃくちゃ大変でした。
画像ファイルから色データをpixel単位で読み取ったり、キーの状態もシグナルハンドラで制御したりと、低レイヤーなところから自分で実装していくので、普段ゲームエンジンを使ってると意識しないようなことを調べ実装しなくてはなりませんでした。
特に、あたり判定などの物理挙動はUnityだと10秒くらいで実装できるので、「Unityってほんとに偉大だなー。」と改めて認識することができました。

明日は、機械学習エンジニアのKotablogさんが経路探索超入門について書いてくれる予定ですので、そちらの記事もお楽しみに!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?