본문 바로가기
Development/멋쟁이사자처럼 게임개발 부트캠프

[멋쟁이사자처럼 Unity 게임 부트캠프 4기] 23일차 - Katana ZERO (4)

by jjeondeuk1008 2025. 4. 8.
반응형

[ 목차 ]


     

     

    23일 차 오늘의 배운 내용은 Katana ZERO 4번째!!!

     

    이번 포스팅에서는 벽 먼지 생성과 적을 배치하고

    적이 플레이어를 감지해서 자동으로 공격하는 것을 실습한다.

     

    드디어 막바지로 온다!

     

     

     

    이전 포스팅은 아래 링크를 첨부해 두었으니, 필요하다면 참고하길 바랍니다!

     

     

     

    Katana ZERO (1)

     

    [멋쟁이사자처럼 Unity 게임 부트캠프 4기] 20일차 - Katana ZERO (1)

    [ 목차 ]  1945에 이어서 다른 게임을 실습해 보는 가진다.  횡스크롤 종류의 KATANA ZERO 게임을 구현해 볼 것이다.2D 횡스크롤은 엄청나게 많은 종류이면서 게임의 기본 중 기본이라고도 볼 수 있

    gang-design.com

     

     

    Katana ZERO (2)

     

    [멋쟁이사자처럼 Unity 게임 부트캠프 4기] 21일차 - Katana ZERO (2)

    [ 목차 ] 지난 포스팅에서 Katana ZERO (1)에 이어서 2번째 포스팅이다. 이번 포스팅은 캐릭터 애니메이션에 추가적으로 이어서 하는 것과디테일과 이펙트를 위주로 진행한다. 전의 포스팅을 보고

    gang-design.com

     

     

    Katana ZERO (3)

     

    [멋쟁이사자처럼 Unity 게임 부트캠프 4기] 22일차 - Katana ZERO (3)

    [ 목차 ] 오늘의 포스팅은 Katana ZERO 3번째 실습이다. 기존에 배웠던 카메라 설정과배경과 캐릭터 추가 설정을 계속 진행해보는 것이다.  이전 포스팅은 아래 첨부로 걸어두었으니 참고 바랍니

    gang-design.com

     

     

     


     

     

     

     

    1. 벽 먼지 생성

     

     

    떨어질 때 벽에 붙어도 떨어지는 모션만 나오는 버그에 대해서 수정

    private void FixedUpdate()
    {
        Debug.DrawRay(pRig2D.position, Vector3.down, new Color(0, 1, 0));
    
        //레이캐스트로 땅체크 
        RaycastHit2D rayHit = Physics2D.Raycast(pRig2D.position, Vector3.down, 1, LayerMask.GetMask("Ground"));
    
        if (pRig2D.linearVelocityY < 0)
        {
            if (rayHit.collider != null)
            {
                if (rayHit.distance < 0.7f)
                {
                    pAnimator.SetBool("Jump", false);
                }
            }
            else
            {
                //떨어지고 있다.
                if(!isWall) //벽이 아닐 때
                {
                    //그냥 떨어지는 중 -> 점프 상태 유지
                    pAnimator.SetBool("Jump", true);
                }
                else
                {
                    //벽티기 -> 벽타기 상태 유지
                    pAnimator.SetBool("Grab", true);
                }
            }
        }
    
    }
    

     

     

     

     

    이제 벽을 탔을 때에도 이펙트가 있으면 디테일해지기 때문에 해당 이미지를 추가한다.

     

     

     

    해당 이미지의 애니메이션을 Hierarchy에 옮겨 애니메이션 클립을 만든 뒤에, 스크립트를 작성할 것이다.

     

    Player 스크립트로 이동한다.

    벽 먼지에 대한 게임오브젝트 변수를 작성한다.

    //벽먼지
    public GameObject walldust;
    

     

     

     

    점프 먼지 시에 이펙트를 넣는 것을 추가한다.

    벽인지 아닌지를 구분하는 if 조건문을 추가해 벽이 아니면 점프 먼지 이펙트가 나오고,

    벽이면 벽 먼지 이펙트를 생성하는 것으로 교체한다.

    //점프 먼지 이펙트
        public void JumpDust()
        {
            if(!isWall) //벽이 아닐 때
            {
                Instantiate(Jdust, transform.position, Quaternion.identity);
            }
            else //벽일 때
            {
                //벽 먼지
                Instantiate(walldust, transform.position, Quaternion.identity);
            }
        }
    

     

     

     

    이제 플레이어 속성에서 해당 프리팹을 넣어준다.

     

     

     

    Update() 메서드 안에 벽점프 먼지 생성에 관한 코드를 추가한다.

    //벽점프 먼지
    GameObject go = Instantiate(walldust, transform.position + new Vector3(0.8f * isRight, 0, 0), Quaternion.identity);
    go.GetComponent<SpriteRenderer>().flipX = sp.flipX;
    

     

     

     

    이것을 캐릭터가 현재 벽에 붙어있는 상태인지 확인하는 if(isWall) 조건문안에 포함한다.

    if(isWall)
    {
        isWallJump = false;
        //벽점프상태
        pRig2D.linearVelocity = new Vector2(pRig2D.linearVelocityX, pRig2D.linearVelocityY * slidingSpeed);
        //벽을 잡고있는 상태에서 점프
        if(Input.GetKeyDown(KeyCode.W))
        {
            isWallJump = true;
            //벽점프 먼지
            GameObject go = Instantiate(walldust, transform.position + new Vector3(0.8f * isRight, 0, 0), Quaternion.identity);
            go.GetComponent<SpriteRenderer>().flipX = sp.flipX;
    
            Invoke("FreezeX", 0.3f);
            //물리
            pRig2D.linearVelocity = new Vector2(-isRight * wallJumpPower, 0.9f * wallJumpPower);
    
            sp.flipX = sp.flipX == false ? true : false;
            isRight = -isRight;
            
        }
    
    }
    

     

     

     

     


     

     

    2. 적 배치

     

     

    적의 이미지를 가져와 배치한다.

     

     

     

    idle 상태의 여러 이미지로 애니메이션 클립애니메이터를 만들어 적 오브젝트에 넣어준다.

    그리고 Box Collider2D도 추가해 적의 이미지와 맞추어준다.

     

     

     

     


     

     

    3. 적의 미사일 생성

     

     

    적의 미사일 코드를 생성하겠다.

    EnemyMissile 이름의 스크립트를 작성한다.

     

    이 코드는 미사일 오브젝트에 붙여줄 예정이다.

     

    변수를 선언한다.

    public float speed = 5f;    //미사일 속도
    public float lifeTime = 3f; //미사일 생존 시간
    public int damage = 10;     //미사일 데미지
    public Vector2 direction;  //미사일 이동 방향
    

     

     

    게임을 시작할 때 일정 시간 후 미사일을 제거하는 코드를 Start() 함수에 포함한다.

    이때 lifeTime 변수를 할당 받는다.

    void Start()
    {
    		Destroy(gameObject, lifeTime);  //일정 시간 후 미사일 제거     
    }
    

     

     

     

    외부에서 방향을 지정할 때 사용한다.

    normalized 정규화를 사용해서 방향만 추출한다. (속도는 일정하게)

    public void SetDirection(Vector2 dir)
    {
        direction = dir.normalized;
    }
    

     

     

    매 프레임마다 미사일을 direction 방향으로 이동한다.

    void Update()
    {
        transform.Translate(direction * speed * Time.deltaTime);
    }
    

     

     

    충돌 관련 코드를 작성한다.

    Player 태그인 오브젝트와 부딪치면 미사일이 삭제된다.

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            // 여기에 플레이어 데미지 로직 추가
            Destroy(gameObject);
        }
    }
    

     

     

     

     

    그렇게 된 EnemyMissile 스크립트 전체 코드이다.

    using UnityEngine;
    
    public class EnemyMissile : MonoBehaviour
    {
        public float speed = 5f;    //미사일 속도
        public float lifeTime = 3f; //미사일 생존 시간
        public int damage = 10;     //미사일 데미지
        public Vector2 direction;  //미사일 이동 방향
       
    
        void Start()
        {
            Destroy(gameObject, lifeTime);  //일정 시간 후 미사일 제거     
        }
    
        public void SetDirection(Vector2 dir)
        {
            direction = dir.normalized;
        }
    
        void Update()
        {
            transform.Translate(direction * speed * Time.deltaTime);
        }
    
        private void OnTriggerEnter2D(Collider2D other)
        {
             if(other.CompareTag("Player"))
            {
                //여기에 플레이어 데미지 로직 추가
                Destroy(gameObject);
            }
        }
    
    }
    

     

     

     

     

    이제 미사일 오브젝트를 생성한다.

     

    해당 미사일 이미지를 가져와 애니메이션 클립과 애니메이터를 만든다.

    Animator와 만들어두었던 Enemy Missile 스크립트, Box Collider 2D를 적용한다.

    여기에서 Is Trigger 체크해둔다.

     

     

    그 미사일 오브젝트는 프리팹을 만들고 Hierachy에서는 삭제해 둔다.

     

     


     

    4. 적 공격 AI

     

     

     

    적이 일정 거리 내에 플레이어를 감지하면, 일정 간격으로 미사일을 발사하는 스크립트를 작성한다.

    이제 적의 캐릭터 속성에 관한 것과 참조 컴포넌트를 할 변수를 선언한다.

    [Header("적 캐릭터 속성")]
    public float detectionRange = 10f;       // 감지 거리
    public float shootingInterval = 2f;      // 미사일 간격
    public GameObject missilePrefab;         // 쏠 미사일 프리팹
    
    [Header("참조 컴포넌트")]
    public Transform firePoint;              // 미사일이 나갈 위치
    private Transform player;                // 플레이어 위치 저장용
    private float shootTimer;                // 쿨타임 타이머
    private SpriteRenderer spriteRenderer;   // 방향 전환용 (좌우)
    

     

     

     

     

    초기 설정으로 player 태그를 찾아서 추적할 오브젝트를 설정한다.

    방향전환을 위한 SpriteRenderer

    쿨타임 타이머 초기화이다.

    void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player").transform;
        spriteRenderer = GetComponent<SpriteRenderer>();
        shootTimer = shootingInterval;
    }
    

     

     

     

     

    Update() 메서드에서는

    플레이어가 존재하는지 체크한 뒤에,

    Distance로 현재 위치와 플레이어 위치 거리 계산을 한다.

     

    플레이어 방향으로 이미지 회전을 적용하고,

     

    쿨타임이 감소되면 자동으로 Shoot() 메서드 호출해 발사하는 것이다.

    void Update()
    {
    	if (player == null) return;     //플레이어가 없으면 실행하지 않음
    		
    	//플레이어와의 거리 계산
    	float distanceToPlayer = Vector2.Distance(transform.position, player.position);
    		
    	if(distanceToPlayer <= detectionRange)
    	{
    		//플레이어 방향으로 스프라이트 회전
    		spriteRenderer.flipX = (player.position.x < transform.position.x);
    				
    				
    		//미사일 발사 로직
    		shootTimer -= Time.deltaTime;   //타이머 감소
    				
    		if(shootTimer <= 0)
    		{
    			Shoot();            //미사일 발사
    			shootTimer = shootingInterval; //타이머 리셋
    		}
    		
    	}
    }

     

     

     

     

    미사일 발사 함수인 Shoot() 메서드이다.

     

    플레이어를 향한 방향으로 일정한 속도를 유지하기 위한 정규화 normalized 사용한다.

    미사일도 마찬가지로 방향을 바꾸는 함수를 적용한다.

    void Shoot()
    {
        GameObject missile = Instantiate(missilePrefab, firePoint.position, Quaternion.identity);
        Vector2 direction = (player.position - firePoint.position).normalized;
        missile.GetComponent<EnemyMissile>().SetDirection(direction);
        missile.GetComponent<SpriteRenderer>().flipX = (player.position.x < transform.position.x);
    }
    

     

     

     

    디버깅용 기즈모 함수이다.

     

    게임 플레이에는 영향이 없으며, 개발을 위해 알아보기 쉽게 작성하는 것이다.

    씬에서 적을 클릭하면 감지 범위를 원으로 표시하는 것이다.

    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, detectionRange);
    }
    

     

     

     

     

    ShootingEnemy 전체 스크립트이다.

    using UnityEngine;
    
    public class ShootingEnemy : MonoBehaviour
    {
        [Header("적 캐릭터 속성")]
        public float detectionRange = 10f;   //플레이어를 감지할 수있는 최대 거리
        public float shootingInterval = 2f;  //미사일 발사 사이의 대기 시간
        public GameObject missilePrefab;     //발사할 미사일 프리팹
    
        [Header("참조 컴포넌트")]
        public Transform firePoint;          //미사일이 발사될 위치
        private Transform player;            //플레이어의 위치 정보
        private float shootTimer;           //발사 타이머
        private SpriteRenderer spriteRenderer; //스프라이트 방향 전환용
    
        void Start()
        {
            //필요한 컴포넌트 초기화
            player = GameObject.FindGameObjectWithTag("Player").transform;
            spriteRenderer = GetComponent<SpriteRenderer>();
            shootTimer = shootingInterval; //타이머 초기화
    
        }
    
        
        void Update()
        {
            if (player == null) return;     //플레이어가 없으면 실행하지 않음
    
            //플레이어와의 거리 계산
            float distanceToPlayer = Vector2.Distance(transform.position, player.position);
    
            if(distanceToPlayer <= detectionRange)
            {
                //플레이어 방향으로 스프라이트 회전
                spriteRenderer.flipX = (player.position.x < transform.position.x);
    
                //미사일 발사 로직
                shootTimer -= Time.deltaTime;   //타이머 감소
    
                if(shootTimer <= 0)
                {
                    Shoot();            //미사일 발사
                    shootTimer = shootingInterval; //타이머 리셋
                }
    
            }
        }
    
        //미사일 발사 함수
        void Shoot()
        {
            //미사일 생성
            GameObject missile = Instantiate(missilePrefab, firePoint.position, Quaternion.identity);
    
            //플레이어 방향으로 발사 방향 설정
            Vector2 direction = (player.position - firePoint.position).normalized;
            missile.GetComponent<EnemyMissile>().SetDirection(direction);
            missile.GetComponent<SpriteRenderer>().flipX = (player.position.x < transform.position.x);
        }
    
        //디버깅용 기즈모
        private void OnDrawGizmosSelected()
        {
            Gizmos.color = Color.red;
            Gizmos.DrawWireSphere(transform.position, detectionRange);
        }
    
    }
    
    

     

     

     

     

    이제 몬스터 오브젝트에 Shooting Enemy 스크립트를 추가한다.

     

    미사일 프리팹을 넣어주고,

    firepoint (미사일이 나갈 위치)를 플레이어의 자식으로 만들어준 뒤에 스크립트에 참조한다.

     

     

     

     


     

     

     

     

    이렇게 KatanaZERO를 마친다!!!!

     

    상당히 기능이 많아서 그런가 전의 실습보단 더 힘들었던 기억이 남아있다.

    그렇지만 횡스크롤의 재미를 느껴본 것 같다.

     

    액션이 많으면 많을수록 힘들어진다는 것도 알았지만..

     

    기억상으로 반절이상이 캐릭터 애니메이션이었던 것 같다..

     

     

    만일에 이것을 직접 디자인을 했다면 얼마나 아찔했을까?!.. 라는 생각도 들긴 했다.

     

     

     

    아무튼 4번째 포스팅으로 KatanaZERO를 끝냈다!


    목차