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 같은 기능은 사용하지 않았다. 스킬 사용 시 현재는 몬스터의 진행방향의 반대로 가도록 했는데, 조이스틱의 방향 값을 활용하여 넉백을 구현하면 자연스럽게 구현할 수 있을 것 같다.