MapManager
우리 그전에 지나갈 수 없는 맵을 서버랑 교환하기 위해 작업을 했다. 이제는 실제로 그 값을 가지고 코드를 추가해 충돌이 일어나게 해보자. 이것을 전역으로 Manager가 조절할 수 있으면 좋겠다고 생각이든다.
MapManager는 실시간으로 맵을 로드하거나 저장하고 갈수 있는 영역과 아닌 영역을 구분하는 역할을 할 것이다.
먼저 일단은 맵을 맵 id에 따라 추가하는 것을 만들었다. 프리팹으로 저장된 Map_000 꼴의 맵들을 불러오는 것이다.
그리고 CanGo 함수를 통해 이 부분이 지나갈 수 있는 영역인지 아닌지를 확인하고 플레이어를 이동하게 하면된다.
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class MapManager
{
public Grid CurrentGrid { get; private set; }
public int MinX { get; set; }
public int MinY { get; set; }
public int MaxX { get; set; }
public int MaxY { get; set; }
bool[,] _collision;
public bool CanGo(Vector3Int cellPos)
{
if (cellPos.x < MinX || cellPos.x > MaxX)
{
return false;
}
if (cellPos.y < MinY || cellPos.y > MaxY)
{
return false;
}
int x = cellPos.x - MinX;
int y = MaxY - cellPos.y;
return !_collision[y, x];
}
public void LoadMap(int mapId)
{
DestoryMap();
string mapName = "Map_" + mapId.ToString("000");
GameObject go = Managers.Resource.Instantiate($"Map/{mapName}");
go.name = "Map";
GameObject collision = Util.FindChild(go, "Tilemap_Collision", true);
if(collision != null)
collision.SetActive(false);
CurrentGrid = go.GetComponent<Grid>();
// Collision 관련 파일
TextAsset txt = Managers.Resource.Load<TextAsset>($"Map/{mapName}");
StringReader reader = new StringReader(txt.text);
MinX = int.Parse(reader.ReadLine());
MaxX = int.Parse(reader.ReadLine());
MinY = int.Parse(reader.ReadLine());
MaxY = int.Parse(reader.ReadLine());
int xCount = MaxX - MinX + 1;
int yCount = MaxY - MinY + 1;
_collision = new bool[yCount, xCount];
for (int y = 0; y < yCount; y++)
{
string line = reader.ReadLine();
for (int x = 0; x < xCount; x++)
{
_collision[y, x] = (line[x] == '1');
}
}
}
public void DestoryMap()
{
GameObject map = GameObject.Find("Map");
if(map != null)
{
GameObject.Destroy(map);
CurrentGrid = null;
}
}
}
바뀐 부분만 작성하면 이렇다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class PlayerController : MonoBehaviour
{
private void LateUpdate()
{
Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, -10);
}
// 이동가능한 상태 일때, 실제 좌표를 이동하게 설정
void UpdateIsMoving()
{
if (_isMoving == false && _dir != MoveDir.None)
{
Vector3Int destPos = _cellPos;
switch (_dir)
{
case MoveDir.Up:
destPos += Vector3Int.up;
break;
case MoveDir.Down:
destPos += Vector3Int.down;
break;
case MoveDir.Left:
destPos += Vector3Int.left;
break;
case MoveDir.Right:
destPos += Vector3Int.right;
break;
}
if (Managers.Map.CanGo(destPos))
{
_cellPos = destPos;
_isMoving = true;
}
}
}
}
Controller 정리
지금은 이제 맵을 구상 완료했고 그 뒤로 플레이어의 움직임까지 완료했으니 이제 몬스터를 만들어볼 차례이다. 그런데 몬스터가 딱히 이미지가 없으므로 플레이어의 sprite를 그대로 사용하되 색상만 다르게 적용해보자.
이제 MonsterController를 만들것인데 플레이어랑 매우 유사한 점이 생각할 수록 많다. 결과적으로 키보드 입력 부분만 제외하면 거의 비슷하다.
그러므로 상위 Class를 만들고 그것에 상속을하자. 상위 Class는 CreatureController로 이름을 짓겠다.
또한 이왕 만드는김에 플레이어가 공격할 때에는 움직일 수 없게하도록 해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class CreatureController : MonoBehaviour
{
public float _speed = 5.0f;
protected Vector3Int _cellPos = Vector3Int.zero;
protected bool _isMoving = false;
protected Animator _animator;
protected SpriteRenderer _sprite;
CreatureState _state = CreatureState.Idle;
public CreatureState State
{
get { return _state; }
set
{
if (_state == value)
return;
_state = value;
UpdateAnimation();
}
}
MoveDir _lastDir = MoveDir.Down;
MoveDir _dir = MoveDir.Down;
public MoveDir Dir
{
get { return _dir; }
set
{
if (_dir == value)
return;
_dir = value;
if (value != MoveDir.None)
_lastDir = value;
UpdateAnimation();
}
}
protected virtual void UpdateAnimation()
{
if(_state == CreatureState.Idle)
{
switch (_lastDir)
{
case MoveDir.Up:
_animator.Play("IDLE_BACK");
_sprite.flipX = false;
break;
case MoveDir.Down:
_animator.Play("IDLE_FRONT");
_sprite.flipX = false;
break;
case MoveDir.Left:
_animator.Play("IDLE_RIGHT");
_sprite.flipX = true;
break;
case MoveDir.Right:
_animator.Play("IDLE_RIGHT");
_sprite.flipX = false;
break;
}
}
else if(_state == CreatureState.Moving)
{
switch (_dir)
{
case MoveDir.Up:
_animator.Play("WALK_BACK");
_sprite.flipX = false;
break;
case MoveDir.Down:
_animator.Play("WALK_FRONT");
_sprite.flipX = false;
break;
case MoveDir.Left:
_animator.Play("WALK_RIGHT");
_sprite.flipX = true;
break;
case MoveDir.Right:
_animator.Play("WALK_RIGHT");
_sprite.flipX = false;
break;
}
}
else if (_state == CreatureState.Skill)
{
// TODO
}
else
{
// TODO
}
}
void Start()
{
Init();
}
void Update()
{
UpdateController();
}
protected virtual void Init()
{
_animator = GetComponent<Animator>();
Vector3 pos = Managers.Map.CurrentGrid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f, 0);
transform.position = pos;
_sprite = GetComponent<SpriteRenderer>();
}
protected virtual void UpdateController()
{
UpdatePosition();
UpdateIsMoving();
}
// 클라단에서 스르륵 움직이게 하기 위함
private void UpdatePosition()
{
if (State != CreatureState.Moving)
return;
Vector3 destPos = Managers.Map.CurrentGrid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f, 0);
Vector3 moveDir = destPos - transform.position;
// 도착 여부 체크
// 목적지까지의 거리 == dist
float dist = moveDir.magnitude;
if (dist < _speed * Time.deltaTime)
{
transform.position = destPos;
// 예외적으로 애니메이션을 직접 컨트롤
_state = CreatureState.Idle;
if (_dir == MoveDir.None)
UpdateAnimation();
}
else
{
transform.position += moveDir.normalized * _speed * Time.deltaTime;
State = CreatureState.Moving;
}
}
// 이동가능한 상태 일때, 실제 좌표를 이동하게 설정
void UpdateIsMoving()
{
if (State == CreatureState.Idle && _dir != MoveDir.None)
{
Vector3Int destPos = _cellPos;
switch (_dir)
{
case MoveDir.Up:
destPos += Vector3Int.up;
break;
case MoveDir.Down:
destPos += Vector3Int.down;
break;
case MoveDir.Left:
destPos += Vector3Int.left;
break;
case MoveDir.Right:
destPos += Vector3Int.right;
break;
}
if (Managers.Map.CanGo(destPos))
{
_cellPos = destPos;
State = CreatureState.Moving;
}
}
}
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class PlayerController : CreatureController
{
protected override void Init()
{
base.Init();
}
protected override void UpdateController()
{
GetDirInput();
base.UpdateController();
}
private void LateUpdate()
{
Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, -10);
}
// 키보드 방향 설정
void GetDirInput()
{
// 이동하겠다고 선언
if (Input.GetKey(KeyCode.W))
{
Dir = MoveDir.Up;
}
else if (Input.GetKey(KeyCode.S))
{
Dir = MoveDir.Down;
}
else if (Input.GetKey(KeyCode.A))
{
Dir = MoveDir.Left;
}
else if (Input.GetKey(KeyCode.D))
{
Dir = MoveDir.Right;
}
else
{
Dir = MoveDir.None;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class MonsterController : CreatureController
{
protected override void Init()
{
base.Init();
}
protected override void UpdateController()
{
//GetDirInput();
base.UpdateController();
}
// 추후 AI를 통해 움직이기 위해 남겨둠
void GetDirInput()
{
// 이동하겠다고 선언
if (Input.GetKey(KeyCode.W))
{
Dir = MoveDir.Up;
}
else if (Input.GetKey(KeyCode.S))
{
Dir = MoveDir.Down;
}
else if (Input.GetKey(KeyCode.A))
{
Dir = MoveDir.Left;
}
else if (Input.GetKey(KeyCode.D))
{
Dir = MoveDir.Right;
}
else
{
Dir = MoveDir.None;
}
}
}
ObjectManager
플레이어와 몬스터 사이에 충돌을 넣을지 말지는 기획적인 영역이지만 그리드 방식으로 움직임을 채택했으니 충돌을 넣는것이 맞다고 생각한다. 따라서 충돌을 해서 서로 못가게 막아주자. 서버에서도 관리를 해줘야하니 클라쪽에서는 간단히 확인만 하자.
그전까지는 우리가 충돌을 MapManager에서 관리하고 있었다. 그럼 유닛과의 충돌을 MapManger에서 관리할 것인지 별도의 스크립트를 둬서 관리할 것인지는 충분히 고려해야한다. 정해진 정답은 없지만 필자는 ObjectManager를 만들어 유닛마다 고유 id를 두어 서로 소통하게끔 할 것이다.
이는 서버와 소통할 때도 편할 것이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ObjectManager
{
//나중에 서버를 할거면 이런식으로 관리
//Dictionary<int, GameObject> _objects = new Dictionary<int, GameObject>();
//클라에서는 단순히 리스트로 관리
List<GameObject> _objects = new List<GameObject>();
public void Add(GameObject go)
{
_objects.Add(go);
}
public void Remove(GameObject go)
{
_objects.Remove(go);
}
// 느리지만 유닛이 많이 없다면 이방식 채택
public GameObject Find(Vector3Int cellPos)
{
foreach (GameObject obj in _objects)
{
CreatureController cc = obj.GetComponent<CreatureController>();
if (cc == null)
{
continue;
}
if (cc.CellPos == cellPos)
return obj;
}
return null;
}
public void Clear()
{
_objects.Clear();
}
}
이런식으로 ObjectManager에서 유닛들을 가지고 있고 플레이어나 Ai, Npc들이 이동할 때 Find 함수를 호출해 충돌이 일어났는지 확인하면 된다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] 컨텐츠 준비 - Monster AI(Patrol AI, Search AI, Skill AI) (0) | 2024.03.05 |
---|---|
[Unity 2D] 컨텐츠 준비 - 스킬(평타, 화살) 사용하기 (1) | 2024.03.05 |
[Unity 2D] 컨텐츠 준비 - 세팅, MapTool, 플레이어 이동 (4) | 2024.02.28 |
[데이터베이스] SQL 튜닝 - 북마크 룩업, Join, Sorting (1) | 2024.02.23 |
[데이터베이스] SQL 튜닝 - 인덱스 분석 (0) | 2024.02.21 |