はじめに
前回は、Tilemapを用いたマップ作成を行いました。
今回は、十字キーによるプレイヤーの移動処理を実装します。
スクリプトの作成
今回は3個のスクリプトを作成します。
Player.cs
まずは1枚目です。
プロジェクトタブで
Create → C# Script
を選択し、新規スクリプトを作成してください。
作成したら、名前を「Player」としてください。
スクリプトを開き、以下のコードをコピー&ペーストしてください。
//Player.cs
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
public class Player : MonoBehaviour
{
[Range(0, 2)] public float MoveSecond = 0.1f;
[SerializeField] public SceneManager SceneManager;
Coroutine _moveCoroutine;
[SerializeField] Vector3Int _pos;
private void Start()
{
if (SceneManager == null) SceneManager = Object.FindObjectOfType<SceneManager>();
if (SceneManager != null && SceneManager.ActiveMap != null)
PutPlayerOnPos(_pos);
_moveCoroutine = StartCoroutine(MoveCoroutine(Position));
if (_moveCoroutine != null) { StopCoroutine(_moveCoroutine); _moveCoroutine = null; }
}
public Vector3Int Position
{
get => _pos;
set
{
if (_pos == value) return;
if (SceneManager == null || SceneManager.ActiveMap == null)
{
_pos = value;
}
else
{
if (_moveCoroutine != null)
{
StopCoroutine(_moveCoroutine);
_moveCoroutine = null;
}
_moveCoroutine = StartCoroutine(MoveCoroutine(value));
}
}
}
public void PutPlayerOnPos(Vector3Int pos)
{
_pos = pos;
if (SceneManager != null && SceneManager.ActiveMap != null)
{
transform.position = SceneManager.ActiveMap.Grid.CellToWorld(pos);
Camera.main.transform.position = transform.position + Vector3.forward * -10 + Vector3.right * -1;
}
}
public bool IsMoving { get => _moveCoroutine != null; }
IEnumerator MoveCoroutine(Vector3Int pos)
{
var sPos = transform.position;
var gPos = SceneManager.ActiveMap.Grid.CellToWorld(pos);
var t = 0f;
while (t < MoveSecond)
{
yield return null;
t += Time.deltaTime;
transform.position = Vector3.Lerp(sPos, gPos, t / MoveSecond);
Camera.main.transform.position = transform.position + Vector3.forward * -10 + Vector3.right * -1;
}
_pos = pos;
_moveCoroutine = null;
}
private void ToNormalPos()
{
if (SceneManager != null && SceneManager.ActiveMap != null)
{
transform.position = SceneManager.ActiveMap.Grid.CellToWorld(Position);
}
}
}
SceneManager.cs
次に2枚目です。
同様にスクリプトを作成し、名前を「SceneManager」としてください。
その後、以下のコードをコピペしてください。
//SceneManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SceneManager : MonoBehaviour
{
public Player Player;
public MapChunk InitialMapPrefab;
public Vector3Int FirstPlayerLocalPos = new Vector3Int(3, 3, 0);
public MapChunk ActiveMap { get; private set; }
List<MapChunk> spawnedChunks = new List<MapChunk>();
Coroutine _currentCoroutine;
void Start()
{
if (Player == null) Player = Object.FindObjectOfType<Player>();
if (InitialMapPrefab != null)
{
InstantiateChunksCenteredOn(InitialMapPrefab, Vector3.zero);
Player.SceneManager = this;
Player.PutPlayerOnPos(FirstPlayerLocalPos);
}
_currentCoroutine = StartCoroutine(MovePlayer());
}
IEnumerator MovePlayer()
{
while (true)
{
if (GetArrow(out var move))
{
var afterPos = Player.Position + move;
bool insideX = afterPos.x >= 0 && afterPos.x < MapChunk.CHUNK_SIZE;
bool insideY = afterPos.y >= 0 && afterPos.y < MapChunk.CHUNK_SIZE;
if (insideX && insideY)
{
var mData = ActiveMap.GetMassData(afterPos);
if (mData.isMovable)
{
Player.Position = afterPos;
yield return new WaitWhile(() => Player.IsMoving);
}
}
else
{
int dx = 0, dy = 0;
if (afterPos.x < 0) dx = -1;
else if (afterPos.x >= MapChunk.CHUNK_SIZE) dx = 1;
if (afterPos.y < 0) dy = -1;
else if (afterPos.y >= MapChunk.CHUNK_SIZE) dy = 1;
var targetPrefab = ActiveMap.GetNeighbor(dx, dy);
if (targetPrefab == null)
{
}
else
{
Vector3Int newLocal = new Vector3Int(Mod(afterPos.x, MapChunk.CHUNK_SIZE), Mod(afterPos.y, MapChunk.CHUNK_SIZE), 0);
var mData = targetPrefab.GetComponent<MapChunk>().GetMassData(newLocal);
if (!mData.isMovable)
{
}
else
{
var savedPlayerWorldPos = Player.transform.position;
float chunkWorldSizeX = ActiveMap.Grid.cellSize.x * MapChunk.CHUNK_SIZE;
float chunkWorldSizeY = ActiveMap.Grid.cellSize.y * MapChunk.CHUNK_SIZE;
Vector3 expectedTargetPos = ActiveMap.transform.position + new Vector3(dx * chunkWorldSizeX, dy * chunkWorldSizeY, 0f);
MapChunk existingTargetInstance = FindSpawnedChunkAtPosition(expectedTargetPos);
if (existingTargetInstance != null)
{
RecenterUsingExistingInstance(existingTargetInstance);
}
else
{
InstantiateChunksCenteredOn(targetPrefab, ActiveMap != null ? ActiveMap.transform.position : Vector3.zero);
}
Player.transform.position = savedPlayerWorldPos;
Camera.main.transform.position = savedPlayerWorldPos + Vector3.forward * -10 + Vector3.right * -1;
Player.Position = newLocal;
yield return new WaitWhile(() => Player.IsMoving);
}
}
}
}
yield return null;
}
}
bool GetArrow(out Vector3Int m)
{
var isMove = false;
m = Vector3Int.zero;
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
m.x -= 1; isMove = true;
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
m.x += 1; isMove = true;
}
else if (Input.GetKeyDown(KeyCode.UpArrow))
{
m.y += 1; isMove = true;
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
m.y -= 1; isMove = true;
}
return isMove;
}
int Mod(int a, int m)
{
int r = a % m;
if (r < 0) r += m;
return r;
}
void InstantiateChunksCenteredOn(MapChunk centerPrefab, Vector3 centerWorldPos)
{
foreach (var c in spawnedChunks) if (c != null) Destroy(c.gameObject);
spawnedChunks.Clear();
ActiveMap = null;
float chunkWorldSizeX = centerPrefab.Grid.cellSize.x * MapChunk.CHUNK_SIZE;
float chunkWorldSizeY = centerPrefab.Grid.cellSize.y * MapChunk.CHUNK_SIZE;
MapChunk centerInstance = Instantiate(centerPrefab.gameObject).GetComponent<MapChunk>();
centerInstance.transform.position = centerWorldPos;
spawnedChunks.Add(centerInstance);
ActiveMap = centerInstance;
for (int dy = 1; dy >= -1; dy--)
{
for (int dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0) continue;
MapChunk neighborPrefab = centerPrefab.GetNeighbor(dx, dy);
if (neighborPrefab == null) continue;
var inst = Instantiate(neighborPrefab.gameObject).GetComponent<MapChunk>();
inst.transform.position = centerWorldPos + new Vector3(dx * chunkWorldSizeX, dy * chunkWorldSizeY, 0f);
spawnedChunks.Add(inst);
}
}
for (int i = 0; i < spawnedChunks.Count; i++)
{
if (spawnedChunks[i] != null)
spawnedChunks[i].gameObject.name = "Chunk_" + i + "_" + spawnedChunks[i].gameObject.name;
}
}
MapChunk FindSpawnedChunkAtPosition(Vector3 pos)
{
const float EPS = 0.01f;
foreach (var c in spawnedChunks)
{
if (c == null) continue;
if (Vector3.Distance(c.transform.position, pos) < EPS) return c;
}
return null;
}
void RecenterUsingExistingInstance(MapChunk newCenter)
{
if (newCenter == null) return;
float chunkWorldSizeX = newCenter.Grid.cellSize.x * MapChunk.CHUNK_SIZE;
float chunkWorldSizeY = newCenter.Grid.cellSize.y * MapChunk.CHUNK_SIZE;
List<MapChunk> newList = new List<MapChunk>();
newList.Add(newCenter);
for (int dy = 1; dy >= -1; dy--)
{
for (int dx = -1; dx <= 1; dx++)
{
if (dx == 0 && dy == 0) continue;
Vector3 expectPos = newCenter.transform.position + new Vector3(dx * chunkWorldSizeX, dy * chunkWorldSizeY, 0f);
MapChunk found = FindSpawnedChunkAtPosition(expectPos);
if (found != null)
{
newList.Add(found);
}
else
{
MapChunk neighborPrefab = newCenter.GetNeighbor(dx, dy);
if (neighborPrefab == null) continue;
var inst = Instantiate(neighborPrefab.gameObject).GetComponent<MapChunk>();
inst.transform.position = expectPos;
newList.Add(inst);
}
}
}
foreach (var old in spawnedChunks)
{
if (old == null) continue;
if (!newList.Contains(old))
{
Destroy(old.gameObject);
}
}
spawnedChunks = newList;
ActiveMap = newCenter;
for (int i = 0; i < spawnedChunks.Count; i++)
{
if (spawnedChunks[i] != null)
spawnedChunks[i].gameObject.name = "Chunk_" + i + "_" + spawnedChunks[i].gameObject.name;
}
}
}
MapChunk.cs
3枚目です。
スクリプトを新規作成後、名前を「MapChunk」としてください。
その後、以下のスクリプトをコピペしてください。
//MapChunk.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class MapChunk : MonoBehaviour
{
public Grid Grid { get => GetComponent<Grid>(); }
Dictionary<string, Tilemap> maps;
readonly static string BACKGROUND_TILEMAP_NAME = "Background";
readonly static string NONE_OBJECTS_TILEMAP_NAME = "Path";
readonly static string OBJECTS_TILEMAP_NAME = "Obstacle";
readonly static string EVENT_BOX_TILEMAP_NAME = "Event";
public MapChunk NeighborTopLeft;
public MapChunk NeighborTop;
public MapChunk NeighborTopRight;
public MapChunk NeighborLeft;
public MapChunk NeighborRight;
public MapChunk NeighborBottomLeft;
public MapChunk NeighborBottom;
public MapChunk NeighborBottomRight;
public const int CHUNK_SIZE = 8;
private void Awake()
{
maps = new Dictionary<string, Tilemap>();
foreach (var tilemap in Grid.GetComponentsInChildren<Tilemap>())
{
if (!maps.ContainsKey(tilemap.name))
maps.Add(tilemap.name, tilemap);
}
}
public MapChunk GetNeighbor(int dx, int dy)
{
if (dx == -1 && dy == 1) return NeighborTopLeft;
if (dx == 0 && dy == 1) return NeighborTop;
if (dx == 1 && dy == 1) return NeighborTopRight;
if (dx == -1 && dy == 0) return NeighborLeft;
if (dx == 1 && dy == 0) return NeighborRight;
if (dx == -1 && dy == -1) return NeighborBottomLeft;
if (dx == 0 && dy == -1) return NeighborBottom;
if (dx == 1 && dy == -1) return NeighborBottomRight;
return null;
}
public Vector3 GetWorldPos(Vector3Int pos)
{
return Grid.CellToWorld(pos);
}
public class MassData
{
public bool isMovable;
public TileBase specialTile;
}
public MassData GetMassData(Vector3Int position)
{
var mass = new MassData();
mass.specialTile = null;
mass.isMovable = true;
if (maps == null) Awake();
if (maps.ContainsKey(EVENT_BOX_TILEMAP_NAME))
mass.specialTile = maps[EVENT_BOX_TILEMAP_NAME].GetTile(position);
if (maps.ContainsKey(OBJECTS_TILEMAP_NAME) && maps[OBJECTS_TILEMAP_NAME].GetTile(position) != null)
{
mass.isMovable = false;
}
else if (!maps.ContainsKey(BACKGROUND_TILEMAP_NAME) || maps[BACKGROUND_TILEMAP_NAME].GetTile(position) == null)
{
mass.isMovable = false;
}
return mass;
}
}
スクリプトのアタッチ
作成したスクリプトをオブジェクトに取り付けていきます。
Player.csはPlayerに
SceneManager.csはSceneManagerに
MapChunk.csは各チャンクプレハブすべてに1つずつ
ドラック&ドロップでアタッチしてください。
インスペクターの設定
Player
「Scene Manager」という欄があるので、シーンにあるSceneManagerをドラック&ドロップしてください。
SceneManager
「Player」という欄に、シーンのPlayerをドラック&ドロップしてください。
「Initial Map Prefab」には、初期チャンクを設定します。ゲーム実行時最初に表示するチャンクプレハブを選び、ここに割り当ててください。
「First Player Local Pos」には、プレイヤーの初期座標を入力します。マップ上の障害物と重ならないような座標を設定してください。
今回は、Chunk1、(x, y)=(3, 3)をそれぞれ設定しています。
MapChunk
各チャンクに対し、隣接しているチャンクを設定します。
前回、以下のような並びになるようチャンクを作成しました。
[1] [2] [3] [4]
[5] [6] [7] [8]
[9] [10] [11] [12]
(数字はチャンク番号)
そのため、例えばChunk1では、
「Neighbor Right」にChunk2を
「Neighbor Bottom」にChunk5を
「Neighbor Bottom Right」にChunk6を割り当てます。
隣接しているチャンクが無い場合は、そのまま空欄にしてください。
この作業をすべてのチャンクプレハブに対し行います。
おわり
今回は、プレイヤーの移動処理を作成しました。
次回は、この処理をざっくり解説していきます。