세팅
기존에 만들었던 C# 서버와 유니티 클라이언트 공부(블로그에 없음)를 위해 제작한 유니티파일을 하나로 합쳐서 진행할 것이다.
버전은 2019.3.15f1을 이용했고 파일 구조는 다음과 같다.
클라이언트와 서버 폴더를 각 이렇게 관리하지만 실제로 큰 작업을 할때는 서버랑 클라를 같이 두진 않긴하다.
사용할 에셋은 유니티 에셋스토어에서 무료로 사용가능한 Tiny RPG과 Tiny RPG Environment를 이용한다.
Tiny RPG - Forest | 2D Characters | Unity Asset Store
Tiny RPG Town Environment | 2D Environments | Unity Asset Store
Map Tool
2D TileMap으로 맵을 그릴 때 싱글게임 인 경우는 크게 상관없지만 멀티 게임인 경우 타일과의 충돌도 중요하다. 특정 지역은 플레이어가 갈 수 없고 접근해서는 안되는 지역이라면 서버쪽에 그 정보를 저장해놓고 플레이어가 이동할때 확인해야만 한다. 물론 Collision을 통해 클라이언트쪽에서도 막을 수 있지만 그렇게만 할 경우 플레이어가 클라이언트 변조를 통해 접근 할 수 있기 때문에 매우 중요한 문제이다.
대부분의 사람들경우 TileMap의 충동을 이용할 때, TileMapCollision을 이용한다. 이는 좋은 방법일 수 있지만 커스터마이징하기가 쉽지 않다. 따라서 새로운 Coliision Layer를 만들고 못가는 영역을 지정한 후 이것을 서버와 교환하는 작업을 해보자.
배경과 플레이어가 지나갈수 없는 Env, 그리고 그것의 충돌을 담당하는 Collision으로 만들어 준다.
바위가 우리가 사용할 Collision이고 이는 실제 플레이에서는 보이지 않는 요소로 제작할 것이다. 제작 단계에서는 눈으로 확인하기 위해 바위로 해놨다. 이 부분을 처리해서 서버에게 갈수없는 영역 좌표를 건네주는 툴을 제작해보자.
Tilemap 컴포넌트가 이미지를 좌표별로 관리를 하고 있을테니 테스트를 해보자.
Test 스크립트를 만들어보자.
우리가 하고자 하는건 타일이 깔려있는 좌표에 대해 다른 포맷, 예를 들어 엑셀이나 xml 파일 등으로 추출할 수 있으면 된다는 것이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class TestCollision : MonoBehaviour
{
public Tilemap _tilemap;
public TileBase _tileBase;
// Start is called before the first frame update
void Start()
{
//타일 설치
_tilemap.SetTile(new Vector3Int(0, 0, 0), _tileBase);
}
// Update is called once per frame
void Update()
{
List<Vector3Int> blocked = new List<Vector3Int>();
foreach(Vector3Int pos in _tilemap.cellBounds.allPositionsWithin)
{
TileBase tile = _tilemap.GetTile(pos);
if (tile != null)
{
blocked.Add(pos);
}
}
}
}
일단 타일이 설치 되어 있는지 없는지는 다음과 같이 확인할 수 있다. 이제 할 것은 버튼을 만들어서 클릭하면 타일의 정보가 다른 포맷으로 추출하게 하는 것을 해보자.
유니티에서 툴을 제작하기 위해서는 특별한 폴더가 필요한데 editor 폴더가 그 것이다.
MapEditor라는 C#파일을 만들어주자.
이는 컴파일할때는 추출되면 안되기 때문에 주의하자.
테스트를 해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class MapEditor
{
#if UNITY_EDITOR
//%(Ctrl), #(Shift), &(Alt)
[MenuItem("Tools/GenerateMap %#g")]
private static void HelloWorld()
{
if (EditorUtility.DisplayDialog("Hello World", "Create?", "Create", "Cancel"))
{
new GameObject("Hello World");
}
}
#endif
}
잘 작동하는 모습이다.
이제 맵을 생성하는 코드를 작성해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class MapEditor
{
#if UNITY_EDITOR
//%(Ctrl), #(Shift), &(Alt)
[MenuItem("Tools/GenerateMap %#g")]
private static void GenerateMap()
{
GameObject go = GameObject.Find("Map");
if (go == null)
return;
Tilemap tm = Util.FindChild<Tilemap>(go, "Tilemap_Collision", true);
List<Vector3Int> blocked = new List<Vector3Int>();
foreach (Vector3Int pos in tm.cellBounds.allPositionsWithin)
{
TileBase tile = tm.GetTile(pos);
if (tile != null)
{
blocked.Add(pos);
}
}
// 갈수 있는 영역 0 아닌 영역 1
using (var writer = File.CreateText("Assets/Resources/Map/output.txt"))
{
writer.WriteLine(tm.cellBounds.xMin);
writer.WriteLine(tm.cellBounds.xMax);
writer.WriteLine(tm.cellBounds.yMin);
writer.WriteLine(tm.cellBounds.yMax);
for(int y = tm.cellBounds.yMax; y >= tm.cellBounds.yMin; y--)
{
for (int x = tm.cellBounds.xMin; x <= tm.cellBounds.xMax; x++)
{
TileBase tile = tm.GetTile(new Vector3Int(x, y, 0));
if (tile != null)
writer.Write("1");
else
writer.Write("0");
}
writer.WriteLine();
}
}
}
#endif
}
이제 이걸 프리팹화 하자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class MapEditor
{
#if UNITY_EDITOR
//%(Ctrl), #(Shift), &(Alt)
[MenuItem("Tools/GenerateMap %#g")]
private static void GenerateMap()
{
GameObject[] gameObjects = Resources.LoadAll<GameObject>("Prefabs/Map");
foreach (GameObject go in gameObjects)
{
Tilemap tm = Util.FindChild<Tilemap>(go, "Tilemap_Collision", true);
List<Vector3Int> blocked = new List<Vector3Int>();
foreach (Vector3Int pos in tm.cellBounds.allPositionsWithin)
{
TileBase tile = tm.GetTile(pos);
if (tile != null)
{
blocked.Add(pos);
}
}
// 갈수 있는 영역 0 아닌 영역 1
using (var writer = File.CreateText($"Assets/Resources/Map/{go.name}.txt"))
{
writer.WriteLine(tm.cellBounds.xMin);
writer.WriteLine(tm.cellBounds.xMax);
writer.WriteLine(tm.cellBounds.yMin);
writer.WriteLine(tm.cellBounds.yMax);
for (int y = tm.cellBounds.yMax; y >= tm.cellBounds.yMin; y--)
{
for (int x = tm.cellBounds.xMin; x <= tm.cellBounds.xMax; x++)
{
TileBase tile = tm.GetTile(new Vector3Int(x, y, 0));
if (tile != null)
writer.Write("1");
else
writer.Write("0");
}
writer.WriteLine();
}
}
}
}
#endif
}
이렇게 해서 Prefabs의 Map 폴더에 우리가 만드는 맵들을 전부 집어넣으면 클릭 한번에 데이터를 전부 생성할 수 있게된다.
플레이어 이동
간단한 플레이어 컨트롤러를 만들어서 이동을 구현해보자. 일단 플레이어가 필요한데, TinyRPG에서 hero를 사용하자.
walk만 사용한다.
플레이어의 이동은 바람의 나라 게임처럼 한칸 한칸 움직이게끔 처리할 것이다. 그런데 진짜 뚝뚝 끊어지면 부자연스러우니까 클라단에서는 자연스럽게 이동하는 것처럼 보이게 할 것이다.
또한 서버쪽에서 이동을 갱신해야하기 때문에 코드를 싱글 게임처럼 구성하진 않을 것이다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class PlayerController : MonoBehaviour
{
public Grid _grid;
public float _speed = 5.0f;
Vector3Int _cellPos = Vector3Int.zero;
MoveDir _dir = MoveDir.None;
bool _isMoving = false;
void Start()
{
Vector3 pos = _grid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f, 0);
transform.position = pos;
}
void Update()
{
// 이동하는 부분과 선언을 나눔
GetDirInput();
UpdatePosition();
UpdateIsMoving();
}
// 키보드 방향 설정
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;
}
}
// 클라단에서 스르륵 움직이게 하기 위함
private void UpdatePosition()
{
if (_isMoving == false)
return;
Vector3 destPos = _grid.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;
_isMoving = false;
}
else
{
transform.position += moveDir.normalized * _speed * Time.deltaTime;
_isMoving = true;
}
}
// 이동가능한 상태 일때, 실제 좌표를 이동하게 설정
void UpdateIsMoving()
{
if (_isMoving == false)
{
switch (_dir)
{
case MoveDir.Up:
_cellPos += Vector3Int.up;
_isMoving = true;
break;
case MoveDir.Down:
_cellPos += Vector3Int.down;
_isMoving = true;
break;
case MoveDir.Left:
_cellPos += Vector3Int.left;
_isMoving = true;
break;
case MoveDir.Right:
_cellPos += Vector3Int.right;
_isMoving = true;
break;
}
}
}
}
이렇게 분리를 함으로써 나중에 서버로 패킷을 보낼때 매우 유용할 것이다. 또한 타일맵 기준으로 한칸의 셀이 이동하기 전까지는 다른 입력을 전부 무시하게끔하여 이동을 막게끔 설계됐다.
플레이어 애니메이션
이제 플레이어가 움직일때 애니메이션을 만들어보자. 이는 서버에서 관심사가 아니기 때문에 편하게 작업하면 된다.
애니메이터를 추가하고 각 애니메이션 walk up side down을 만들어주면 된다.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class PlayerController : MonoBehaviour
{
public Grid _grid;
public float _speed = 5.0f;
Vector3Int _cellPos = Vector3Int.zero;
bool _isMoving = false;
Animator _animator;
MoveDir _dir = MoveDir.Down;
public MoveDir Dir
{
get { return _dir; }
set
{
if (_dir == value)
return;
switch (value)
{
case MoveDir.Up:
_animator.Play("WALK_BACK");
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
break;
case MoveDir.Down:
_animator.Play("WALK_FRONT");
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
break;
case MoveDir.Left:
_animator.Play("WALK_RIGHT");
transform.localScale = new Vector3(-1.0f, 1.0f, 1.0f);
break;
case MoveDir.Right:
_animator.Play("WALK_RIGHT");
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
break;
case MoveDir.None:
if(_dir == MoveDir.Up)
{
_animator.Play("IDLE_BACK");
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
}
else if (_dir == MoveDir.Down)
{
_animator.Play("IDLE_FRONT");
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
}
else if (_dir == MoveDir.Left)
{
_animator.Play("IDLE_RIGHT");
transform.localScale = new Vector3(-1.0f, 1.0f, 1.0f);
}
else
{
_animator.Play("IDLE_RIGHT");
transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
}
break;
}
_dir = value;
}
}
void Start()
{
_animator = GetComponent<Animator>();
Vector3 pos = _grid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f, 0);
transform.position = pos;
}
void Update()
{
// 이동하는 부분과 선언을 나눔
GetDirInput();
UpdatePosition();
UpdateIsMoving();
}
// 키보드 방향 설정
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;
}
}
// 클라단에서 스르륵 움직이게 하기 위함
private void UpdatePosition()
{
if (_isMoving == false)
return;
Vector3 destPos = _grid.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;
_isMoving = false;
}
else
{
transform.position += moveDir.normalized * _speed * Time.deltaTime;
_isMoving = true;
}
}
// 이동가능한 상태 일때, 실제 좌표를 이동하게 설정
void UpdateIsMoving()
{
if (_isMoving == false)
{
switch (_dir)
{
case MoveDir.Up:
_cellPos += Vector3Int.up;
_isMoving = true;
break;
case MoveDir.Down:
_cellPos += Vector3Int.down;
_isMoving = true;
break;
case MoveDir.Left:
_cellPos += Vector3Int.left;
_isMoving = true;
break;
case MoveDir.Right:
_cellPos += Vector3Int.right;
_isMoving = true;
break;
}
}
}
}
수정된 플레이어 컨트롤러이다.
잘 작동하는 모습을 확인할 수 있다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] 컨텐츠 준비 - 스킬(평타, 화살) 사용하기 (1) | 2024.03.05 |
---|---|
[Unity 2D] 컨텐츠 준비 - MapManager, Controller 정리, ObjectManager (0) | 2024.03.04 |
[데이터베이스] SQL 튜닝 - 북마크 룩업, Join, Sorting (1) | 2024.02.23 |
[데이터베이스] SQL 튜닝 - 인덱스 분석 (0) | 2024.02.21 |
[데이터베이스] SQL 입문 - 정규화, INDEX, UNION, JOIN... (0) | 2024.02.20 |