Unity2D
Unity) [유니티 2D] 길 찾기 + 넉백
HSH12345
2023. 3. 29. 03:03
using System.Collections;
using UnityEngine.EventSystems;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class PathFinding : MonoBehaviour
{
[Header("Path Finding")]
public GameObject target;
// 맵을 격자로 분할한다.
Grid grid;
// 남은거리를 넣을 큐 생성.
public Queue<Vector2> wayQueue = new Queue<Vector2>();
private Coroutine findPathRoutine;
[Header("Player Ctrl")]
//rigidbody
private Rigidbody2D rBody2D;
// 뭔가와 상호작용 하고 있을때는 walkable이 false 가 됨.
public static bool walkable = true;
// 플레이어 이동/회전 속도 등 저장할 변수
public float moveSpeed;
// 장애물/NPC 판단시 멈추게 할 범위
public float eyeRange;
public float attackRange;
public bool isWalk;
public bool isWalking;
//타겟과 위치값 비교
private Vector2 dir;
private float dirX;
//넉백 관련
private bool isKnockBack;
private float knockBackSpeed;
private Vector2 knockBackDir;
private float stun = 1;
private Coroutine knockBackRoutine;
private void Awake()
{
// 격자 생성
this.grid = GameObject.Find("Astar").GetComponent<Grid>();
//grid = GetComponent<Grid>();
walkable = true;
//rigidbody
this.rBody2D = gameObject.GetComponent<Rigidbody2D>();
}
private void Start()
{
// 초깃값 초기화.
this.isWalking = false;
}
private void FixedUpdate()
{
this.StartFindPath((Vector2)this.transform.position, (Vector2)this.target.transform.position);
//스프라이트 플립
this.FlipEnemy();
if (this.isKnockBack) this.KnockBack(this.knockBackSpeed);
}
// start to target 이동.
public void StartFindPath(Vector2 startPos, Vector2 targetPos)
{
if(this.findPathRoutine != null) this.StopCoroutine(this.findPathRoutine);
this.findPathRoutine = this.StartCoroutine(this.FindPath(startPos, targetPos));
}
// 길찾기 로직.
private IEnumerator FindPath(Vector2 startPos, Vector2 targetPos)
{
// start, target의 좌표를 grid로 분할한 좌표로 지정.
Node startNode = grid.NodeFromWorldPoint(startPos);
Node targetNode = grid.NodeFromWorldPoint(targetPos);
// target에 도착했는지 확인하는 변수.
bool pathSuccess = false;
if (!startNode.walkable)
Debug.Log("Unwalkable StartNode 입니다.");
// walkable한 targetNode인 경우 길찾기 시작.
if(targetNode.walkable)
{
// openSet, closedSet 생성.
// closedSet은 이미 계산 고려한 노드들.
// openSet은 계산할 가치가 있는 노드들.
List<Node> openSet = new List<Node>();
HashSet<Node> closedSet = new HashSet<Node>();
openSet.Add(startNode);
// closedSet에서 가장 최저의 F를 가지는 노드를 빼낸다.
while (openSet.Count > 0)
{
// currentNode를 계산 후 openSet에서 빼야 한다.
Node currentNode = openSet[0];
// 모든 openSet에 대해, current보다 f값이 작거나, h(휴리스틱)값이 작으면 그것을 current로 지정.
for (int i = 1; i < openSet.Count; i++)
{
if (openSet[i].fCost < currentNode.fCost || openSet[i].fCost == currentNode.fCost && openSet[i].hCost < currentNode.hCost)
currentNode = openSet[i];
}
// openSet에서 current를 뺀 후, closedSet에 추가.
openSet.Remove(currentNode);
closedSet.Add(currentNode);
// 방금 들어온 노드가 목적지 인 경우
if (currentNode == targetNode)
{
// seeker가 위치한 지점이 target이 아닌 경우
if(pathSuccess == false)
{
// wayQueue에 PATH를 넣어준다.
PushWay( RetracePath(startNode, targetNode) ) ;
}
pathSuccess = true;
break;
}
// current의 상하좌우 노드들에 대하여 g,h cost를 고려한다.
foreach (Node neighbour in grid.GetNeighbours(currentNode))
{
if (!neighbour.walkable || closedSet.Contains(neighbour)) continue;
// F cost 생성.
int newMovementCostToNeighbour = currentNode.gCost + GetDistance(currentNode, neighbour);
// 이웃으로 가는 F cost가 이웃의 G보다 짧거나, 방문해볼 Openset에 그 값이 없다면,
if (newMovementCostToNeighbour < neighbour.gCost || !openSet.Contains(neighbour))
{
neighbour.gCost = newMovementCostToNeighbour;
neighbour.hCost = GetDistance(neighbour, targetNode);
neighbour.parent = currentNode;
// openSet에 추가.
if (!openSet.Contains(neighbour)) openSet.Add(neighbour);
}
}
}
}
yield return null;
// 길을 찾았을 경우(계산 다 끝난경우) 이동시킴.
if(pathSuccess == true)
{
// 이동중이라는 변수 ON
this.isWalking = true;
// wayQueue를 따라 이동시킨다.
while (this.wayQueue.Count > 0)
{
this.dir = this.wayQueue.First() - (Vector2)this.transform.position;
//길찾기 조건 - 탐색 범위
if (Vector2.Distance(this.transform.position, this.target.transform.position) <= this.eyeRange)
{
this.rBody2D.velocity = dir.normalized * moveSpeed * 5 * Time.deltaTime * this.stun;
}
if ((Vector2)this.transform.position == this.wayQueue.First())
{
Debug.Log("Dequeue");
this.wayQueue.Dequeue();
}
yield return new WaitForSeconds(0.02f);
}
// 이동중이라는 변수 OFF
this.isWalking = false;
}
}
// WayQueue에 새로운 PATH를 넣어준다.
private void PushWay(Vector2[] array)
{
this.wayQueue.Clear();
foreach (Vector2 item in array) this.wayQueue.Enqueue(item);
}
// 현재 큐에 거꾸로 저장되어있으므로, 역순으로 wayQueue를 뒤집어준다.
private Vector2[] RetracePath(Node startNode, Node endNode)
{
List<Node> path = new List<Node>();
Node currentNode = endNode;
while(currentNode != startNode)
{
path.Add(currentNode);
currentNode = currentNode.parent;
}
path.Reverse();
// Grid의 path에 찾은 길을 등록한다.
this.grid.path = path;
Vector2[] wayPoints = SimplifyPath(path);
return wayPoints;
}
// Node에서 Vector 정보만 빼낸다.
private Vector2[] SimplifyPath(List<Node> path)
{
List<Vector2> wayPoints = new List<Vector2>();
for(int i = 0; i < path.Count; i++)
{
wayPoints.Add(path[i].worldPosition);
}
return wayPoints.ToArray();
}
// custom g cost 또는 휴리스틱 추정치를 계산하는 함수.
// 매개변수로 들어오는 값에 따라 기능이 바뀝니다.
private int GetDistance(Node nodeA, Node nodeB)
{
int dstX = Mathf.Abs(nodeA.gridX - nodeB.gridX);
int dstY = Mathf.Abs(nodeA.gridY - nodeB.gridY);
// 대각선 - 14, 상하좌우 - 10.
if (dstX > dstY) return 14 * dstY + 10 * (dstX - dstY);
return 14 * dstX + 10 * (dstY - dstX);
}
private void FlipEnemy()
{
this.dirX = this.transform.position.x - this.target.transform.position.x;
if (dirX > 0) this.GetComponent<SpriteRenderer>().flipX = true;
else this.GetComponent<SpriteRenderer>().flipX = false;
}
//넉백 효과
private void OnTriggerEnter2D(Collider2D collision)
{
var bullet = collision.GetComponent<Bullet>();
var skill = collision.GetComponent<Skill>();
//this.knockBackDir = this.transform.position - collision.transform.position;
Debug.Log(this.knockBackDir);
if(this.knockBackRoutine != null) this.StopCoroutine(this.knockBackRoutine);
if (collision.CompareTag("PlayerBullet"))
{
if (bullet != null)
{
//탄환의 진행 방향으로 넉백
this.knockBackDir = bullet.Dir;
this.knockBackRoutine = this.StartCoroutine(this.KnockbackRoutine());
this.knockBackSpeed = bullet.knockBackSpeed;
}
else if(skill != null)
{
this.knockBackDir = -this.dir;
this.knockBackRoutine = this.StartCoroutine(this.KnockbackRoutine());
this.knockBackSpeed = skill.knockBackSpeed;
}
}
}
private IEnumerator KnockbackRoutine()
{
this.isKnockBack = true;
this.stun = 0;
yield return new WaitForSeconds(0.3f);
this.stun = 1;
this.isKnockBack = false;
}
private void KnockBack(float knockbackSpeed)
{
this.rBody2D.AddForce(this.knockBackDir.normalized * this.knockBackSpeed * 80 * Time.deltaTime);
}
}
bool 타입 변수와 코루틴을 활용하여 넉백이 발생했을 때 몬스터가 이동을 멈추고 탄환의 방향으로 넉백되도록 하였다. 플레이어에게 넉백 능력치가 있기 때문에 ForceMode2D.Impulse 같은 기능은 사용하지 않았다. 스킬 사용 시 현재는 몬스터의 진행방향의 반대로 가도록 했는데, 조이스틱의 방향 값을 활용하여 넉백을 구현하면 자연스럽게 구현할 수 있을 것 같다.