Patrol AI
기본적인 몬스터 AI를 만들어 볼텐데 AI 기능이 엄청 많아지는 경우에는 AI 클래스를 따로 설계해서 추가하는 것이 좋고 그게 아니라면 MonsterController에 추가하는 것이 좋다. 하지만 결국에는 서버에서 연산을 하고 클라는 통보만 받기때문에 나중에는 서버쪽으로 관련 코드를 옮겨야 할 것이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class MonsterController : CreatureController
{
Coroutine _coPatrol;
Vector3Int _destCellPos;
public override CreatureState State
{
get { return _state; }
set
{
if (_state == value)
return;
base.State = value;
if (_coPatrol != null)
{
StopCoroutine(_coPatrol);
_coPatrol = null;
}
}
}
protected override void Init()
{
base.Init();
State = CreatureState.Idle;
Dir = MoveDir.None;
}
protected override void UpdateController()
{
base.UpdateController();
}
// 추후 AI를 통해 움직이기 위해 남겨둠
protected override void UpdateIdle()
{
base.UpdateIdle();
if (_coPatrol == null) {
_coPatrol = StartCoroutine("CoPatrol");
}
}
protected override void MoveToNextPos()
{
Vector3Int moveCellDir = _destCellPos - CellPos;
if (moveCellDir.x > 0)
Dir = MoveDir.Right;
else if (moveCellDir.x < 0)
Dir = MoveDir.Left;
else if (moveCellDir.y > 0)
Dir = MoveDir.Up;
else if (moveCellDir.y < 0)
Dir = MoveDir.Down;
else
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) && Managers.Object.Find(destPos) == null)
{
CellPos = destPos;
}
else
State = CreatureState.Idle;
}
public override void OnDamaged()
{
GameObject effect = Managers.Resource.Instantiate("Effect/DieEffect");
effect.transform.position = transform.position;
effect.GetComponent<Animator>().Play("START");
GameObject.Destroy(effect, 0.5f);
Managers.Object.Remove(gameObject);
Managers.Resource.Destroy(gameObject);
}
IEnumerator CoPatrol()
{
int waitSeconds = Random.Range(1, 4);
yield return new WaitForSeconds(waitSeconds);
for(int i = 0; i < 10; i++)
{
int xRange = Random.Range(-5, 6);
int yRange = Random.Range(-5, 6);
Vector3Int randPos = CellPos + new Vector3Int(xRange, yRange, 0);
if(Managers.Map.CanGo(randPos) && Managers.Object.Find(randPos) == null)
{
_destCellPos = randPos;
State = CreatureState.Moving;
yield break;
}
}
State = CreatureState.Idle;
}
}
몬스터 컨트롤러에서 IDLE 상태에서 코루틴을 호출한다음 랜덤하게 좌표를 지정해주는 역할을 한다.
시연 영상을 AI를 전부 만든 후에 올리겠다.
Search AI
위의 움직임은 무작위로 길을 걸어가게 작동하지만 그게 아니라 a* 알고리즘을 적용해 길을 찾아보도록하자. 또한 길을 찾아 걸어다니는 동안 플레이어를 찾게되면 지금 하는 행동을 포기하고 플레이어를 추격하는 것을 만들어보자.
길 찾기를 클라에서 하는 것은 올바른가? 이거는 게임의 설계마다 다르다.
일단 싱글게임이라 가정하고 길찾기를 진행하겠다.
MapManager에 A*알고리즘을 추가해준다.
#region A* PathFinding
// U D L R
int[] _deltaY = new int[] { 1, -1, 0, 0 };
int[] _deltaX = new int[] { 0, 0, -1, 1 };
int[] _cost = new int[] { 10, 10, 10, 10 };
public List<Vector3Int> FindPath(Vector3Int startCellPos, Vector3Int destCellPos, bool ignoreDestCollision = false)
{
List<Pos> path = new List<Pos>();
// 점수 매기기
// F = G + H
// F = 최종 점수 (작을 수록 좋음, 경로에 따라 달라짐)
// G = 시작점에서 해당 좌표까지 이동하는데 드는 비용 (작을 수록 좋음, 경로에 따라 달라짐)
// H = 목적지에서 얼마나 가까운지 (작을 수록 좋음, 고정)
// (y, x) 이미 방문했는지 여부 (방문 = closed 상태)
bool[,] closed = new bool[SizeY, SizeX]; // CloseList
// (y, x) 가는 길을 한 번이라도 발견했는지
// 발견X => MaxValue
// 발견O => F = G + H
int[,] open = new int[SizeY, SizeX]; // OpenList
for (int y = 0; y < SizeY; y++)
for (int x = 0; x < SizeX; x++)
open[y, x] = Int32.MaxValue;
Pos[,] parent = new Pos[SizeY, SizeX];
// 오픈리스트에 있는 정보들 중에서, 가장 좋은 후보를 빠르게 뽑아오기 위한 도구
PriorityQueue<PQNode> pq = new PriorityQueue<PQNode>();
// CellPos -> ArrayPos
Pos pos = Cell2Pos(startCellPos);
Pos dest = Cell2Pos(destCellPos);
// 시작점 발견 (예약 진행)
open[pos.Y, pos.X] = 10 * (Math.Abs(dest.Y - pos.Y) + Math.Abs(dest.X - pos.X));
pq.Push(new PQNode() { F = 10 * (Math.Abs(dest.Y - pos.Y) + Math.Abs(dest.X - pos.X)), G = 0, Y = pos.Y, X = pos.X });
parent[pos.Y, pos.X] = new Pos(pos.Y, pos.X);
while (pq.Count > 0)
{
// 제일 좋은 후보를 찾는다
PQNode node = pq.Pop();
// 동일한 좌표를 여러 경로로 찾아서, 더 빠른 경로로 인해서 이미 방문(closed)된 경우 스킵
if (closed[node.Y, node.X])
continue;
// 방문한다
closed[node.Y, node.X] = true;
// 목적지 도착했으면 바로 종료
if (node.Y == dest.Y && node.X == dest.X)
break;
// 상하좌우 등 이동할 수 있는 좌표인지 확인해서 예약(open)한다
for (int i = 0; i < _deltaY.Length; i++)
{
Pos next = new Pos(node.Y + _deltaY[i], node.X + _deltaX[i]);
// 유효 범위를 벗어났으면 스킵
// 벽으로 막혀서 갈 수 없으면 스킵
if (!ignoreDestCollision || next.Y != dest.Y || next.X != dest.X)
{
if (CanGo(Pos2Cell(next)) == false) // CellPos
continue;
}
// 이미 방문한 곳이면 스킵
if (closed[next.Y, next.X])
continue;
// 비용 계산
int g = 0;// node.G + _cost[i];
int h = 10 * ((dest.Y - next.Y) * (dest.Y - next.Y) + (dest.X - next.X) * (dest.X - next.X));
// 다른 경로에서 더 빠른 길 이미 찾았으면 스킵
if (open[next.Y, next.X] < g + h)
continue;
// 예약 진행
open[dest.Y, dest.X] = g + h;
pq.Push(new PQNode() { F = g + h, G = g, Y = next.Y, X = next.X });
parent[next.Y, next.X] = new Pos(node.Y, node.X);
}
}
return CalcCellPathFromParent(parent, dest);
}
List<Vector3Int> CalcCellPathFromParent(Pos[,] parent, Pos dest)
{
List<Vector3Int> cells = new List<Vector3Int>();
int y = dest.Y;
int x = dest.X;
while (parent[y, x].Y != y || parent[y, x].X != x)
{
cells.Add(Pos2Cell(new Pos(y, x)));
Pos pos = parent[y, x];
y = pos.Y;
x = pos.X;
}
cells.Add(Pos2Cell(new Pos(y, x)));
cells.Reverse();
return cells;
}
Pos Cell2Pos(Vector3Int cell)
{
// CellPos -> ArrayPos
return new Pos(MaxY - cell.y, cell.x - MinX);
}
Vector3Int Pos2Cell(Pos pos)
{
// ArrayPos -> CellPos
return new Vector3Int(pos.X + MinX, MaxY - pos.Y, 0);
}
#endregion
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class MonsterController : CreatureController
{
Coroutine _coPatrol;
Coroutine _coSearch;
Vector3Int _destCellPos;
GameObject _target;
float _searchRange = 5.0f;
public override CreatureState State
{
get { return _state; }
set
{
if (_state == value)
return;
base.State = value;
if (_coPatrol != null)
{
StopCoroutine(_coPatrol);
_coPatrol = null;
}
if(_coSearch != null)
{
StopCoroutine(_coSearch);
_coSearch = null;
}
}
}
protected override void Init()
{
base.Init();
State = CreatureState.Idle;
Dir = MoveDir.None;
_speed = 3.0f;
}
protected override void UpdateController()
{
base.UpdateController();
}
// 추후 AI를 통해 움직이기 위해 남겨둠
protected override void UpdateIdle()
{
base.UpdateIdle();
if (_coPatrol == null)
{
_coPatrol = StartCoroutine("CoPatrol");
}
if (_coSearch == null)
{
_coSearch = StartCoroutine("CoSearch");
}
}
protected override void MoveToNextPos()
{
Vector3Int destPos = _destCellPos;
if(_target != null)
{
destPos = _target.GetComponent<CreatureController>().CellPos;
}
List<Vector3Int> path = Managers.Map.FindPath(CellPos, destPos, ignoreDestCollision: true);
if(path.Count < 2 || (_target != null && path.Count > 10))
{
_target = null;
State = CreatureState.Idle;
return;
}
Vector3Int nextPos = path[1];
Vector3Int moveCellDir = nextPos - CellPos;
if (moveCellDir.x > 0)
Dir = MoveDir.Right;
else if (moveCellDir.x < 0)
Dir = MoveDir.Left;
else if (moveCellDir.y > 0)
Dir = MoveDir.Up;
else if (moveCellDir.y < 0)
Dir = MoveDir.Down;
else
Dir = MoveDir.None;
if (Managers.Map.CanGo(nextPos) && Managers.Object.Find(nextPos) == null)
{
CellPos = nextPos;
}
else
State = CreatureState.Idle;
}
public override void OnDamaged()
{
GameObject effect = Managers.Resource.Instantiate("Effect/DieEffect");
effect.transform.position = transform.position;
effect.GetComponent<Animator>().Play("START");
GameObject.Destroy(effect, 0.5f);
Managers.Object.Remove(gameObject);
Managers.Resource.Destroy(gameObject);
}
IEnumerator CoPatrol()
{
int waitSeconds = Random.Range(1, 4);
yield return new WaitForSeconds(waitSeconds);
for(int i = 0; i < 10; i++)
{
int xRange = Random.Range(-5, 6);
int yRange = Random.Range(-5, 6);
Vector3Int randPos = CellPos + new Vector3Int(xRange, yRange, 0);
if(Managers.Map.CanGo(randPos) && Managers.Object.Find(randPos) == null)
{
_destCellPos = randPos;
State = CreatureState.Moving;
yield break;
}
}
State = CreatureState.Idle;
}
IEnumerator CoSearch()
{
while (true)
{
yield return new WaitForSeconds(1);
if (_target != null)
continue;
_target = Managers.Object.Find((go) =>
{
PlayerController pc = go.GetComponent<PlayerController>();
if (pc == null)
return false;
Vector3Int dir = (pc.CellPos - CellPos);
if (dir.magnitude > _searchRange)
return false;
return true;
});
}
}
}
Skill AI
이제 플레이어를 추격할 수 있게 됐으니 플레이어에게 일정 거리가 됐을 때 공격을 하게 해보자.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;
public class MonsterController : CreatureController
{
Coroutine _coPatrol;
Coroutine _coSearch;
Coroutine _coSkill;
Vector3Int _destCellPos;
GameObject _target;
float _searchRange = 10.0f;
float _skillRange = 1.0f;
bool _rangeSkill = false;
public override CreatureState State
{
get { return _state; }
set
{
if (_state == value)
return;
base.State = value;
if (_coPatrol != null)
{
StopCoroutine(_coPatrol);
_coPatrol = null;
}
if(_coSearch != null)
{
StopCoroutine(_coSearch);
_coSearch = null;
}
}
}
protected override void Init()
{
base.Init();
State = CreatureState.Idle;
Dir = MoveDir.None;
_speed = 3.0f;
_rangeSkill = (Random.Range(0, 2) == 0 ? true : false);
if (_rangeSkill) _skillRange = 10.0f;
else _skillRange = 1.0f;
}
protected override void UpdateController()
{
base.UpdateController();
}
// 추후 AI를 통해 움직이기 위해 남겨둠
protected override void UpdateIdle()
{
base.UpdateIdle();
if (_coPatrol == null)
{
_coPatrol = StartCoroutine("CoPatrol");
}
if (_coSearch == null)
{
_coSearch = StartCoroutine("CoSearch");
}
}
protected override void MoveToNextPos()
{
Vector3Int destPos = _destCellPos;
if(_target != null)
{
destPos = _target.GetComponent<CreatureController>().CellPos;
Vector3Int dir = destPos - CellPos;
if(dir.magnitude <= _skillRange &&(dir.x == 0 || dir.y == 0))
{
Dir = GetDirFormVec(dir);
State = CreatureState.Skill;
if (_rangeSkill)
_coSkill = StartCoroutine("CoStartShootArrow");
else
_coSkill = StartCoroutine("CoStartPunch");
return;
}
}
List<Vector3Int> path = Managers.Map.FindPath(CellPos, destPos, ignoreDestCollision: true);
if(path.Count < 2 || (_target != null && path.Count > 20))
{
_target = null;
State = CreatureState.Idle;
return;
}
Vector3Int nextPos = path[1];
Vector3Int moveCellDir = nextPos - CellPos;
Dir = GetDirFormVec(moveCellDir);
if (Managers.Map.CanGo(nextPos) && Managers.Object.Find(nextPos) == null)
{
CellPos = nextPos;
}
else
State = CreatureState.Idle;
}
public override void OnDamaged()
{
GameObject effect = Managers.Resource.Instantiate("Effect/DieEffect");
effect.transform.position = transform.position;
effect.GetComponent<Animator>().Play("START");
GameObject.Destroy(effect, 0.5f);
Managers.Object.Remove(gameObject);
Managers.Resource.Destroy(gameObject);
}
IEnumerator CoPatrol()
{
int waitSeconds = Random.Range(1, 4);
yield return new WaitForSeconds(waitSeconds);
for(int i = 0; i < 10; i++)
{
int xRange = Random.Range(-5, 6);
int yRange = Random.Range(-5, 6);
Vector3Int randPos = CellPos + new Vector3Int(xRange, yRange, 0);
if(Managers.Map.CanGo(randPos) && Managers.Object.Find(randPos) == null)
{
_destCellPos = randPos;
State = CreatureState.Moving;
yield break;
}
}
State = CreatureState.Idle;
}
IEnumerator CoSearch()
{
while (true)
{
yield return new WaitForSeconds(1);
if (_target != null)
continue;
_target = Managers.Object.Find((go) =>
{
PlayerController pc = go.GetComponent<PlayerController>();
if (pc == null)
return false;
Vector3Int dir = (pc.CellPos - CellPos);
if (dir.magnitude > _searchRange)
return false;
return true;
});
}
}
IEnumerator CoStartPunch()
{
// 피격 판정
GameObject go = Managers.Object.Find(GetFrontCellPos());
if (go != null)
{
CreatureController cc = go.GetComponent<CreatureController>();
if (cc != null)
cc.OnDamaged();
}
yield return new WaitForSeconds(0.5f);
State = CreatureState.Moving;
_coSkill = null;
}
IEnumerator CoStartShootArrow()
{
GameObject go = Managers.Resource.Instantiate("Creature/Arrow");
ArrowController ac = go.GetComponent<ArrowController>();
ac.Dir = _lastDir;
ac.CellPos = CellPos;
// 대기시간
yield return new WaitForSeconds(0.3f);
State = CreatureState.Moving;
_coSkill = null;
}
}
이제부터 할 것은 클라이언트에서 동작하던 것을 서버로 옮겨서 동기화를 해볼 것이다. 클라이언트는 60프레임 넘게 연산을 하지만 서버는 그렇게 자주 하지 못한다. 해봐야 1초에 3,4번? 정도 하게 되는데 그정도 연산을 가지고 동기화를 해줘야하기 때문에 신경써야하는 부분도 많다.
'Unity > 온라인 RPG' 카테고리의 다른 글
[Unity 2D] 서버 연동 - MyPlayer 분리 및 이동 동기화 (0) | 2024.03.08 |
---|---|
[Unity 2D] 서버 연동 - 멀티플레이 환경 및 게임 입장 (1) | 2024.03.07 |
[Unity 2D] 컨텐츠 준비 - 스킬(평타, 화살) 사용하기 (1) | 2024.03.05 |
[Unity 2D] 컨텐츠 준비 - MapManager, Controller 정리, ObjectManager (0) | 2024.03.04 |
[Unity 2D] 컨텐츠 준비 - 세팅, MapTool, 플레이어 이동 (4) | 2024.02.28 |