본문 바로가기
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를 끝냈다!


목차