Project Emerald (Horizontal Scroll Game) - UAVisonline/Portfolio GitHub Wiki

횡스크롤 액션 게임 제작

Project Emerald는 '모모도라 : 달 아래의 진혼곡'과 같은 횡스크롤 액션게임을 만들고자 하는 목적으로 제작한 Game이다. 해당 Game은 플레이어를 조종해, 적을 없애며 세상을 탐험하는 것을 목표로 삼으며 이러한 부분에 있어서 사용자의 컨트롤 요소를 자극하도록 제작되었다.

해당 프로젝트는 2019년 전역 후, 유니티에 다시 적응하고자 제작을 시작하였다. 또한 필자의 실력에 비해 기획한 게임이 매우 방대하여서 체험이 가능한 정도로만 제작 후 차후 다시 제작할 것을 목표로 하여 개발을 중지했다.

해당 프로젝트를 진행하면서 사용한 그래픽은 직접 임시로 제작하였으며, 사운드와 같은 부분은 freesound.org에서 다운로드해 사용하였다.

해당 프로젝트에 대한 위키에서는 아래와 같은 부분에 대해 설명하고자 한다.

  • 플레이어 캐릭터 조작을 위해 스크립트를 어떻게 작성하였는가?
  • 적을 구현하기 위해 스크립트를 어떻게 작성하였는가?
  • 지금 시점에서 프로젝트의 아쉬운 부분은 무엇인가? 이를 어떻게 수정해야 좋을까?

1. 플레이어 캐릭터 조작에 관하여

1.플레이어의 움직임을 어떻게 처리하는가? (PlayerController Script)

플레이어 캐릭터 조작을 담당하는 PlayerController Script는 물리적인 값을 가지는 움직임에 대해서 매 프레임마다 호출하는 Update 함수가 아닌 일정 시간간격마다 호출되는 FixedUpdate를 사용해 처리하도록 짜여져 있다.

플레이어 캐릭터가 땅 위에 있어서 움직일 수 있을때, 땅 위에 있지만 공격을 하고 있을 때, 땅 위에 있지않고 떨어지거나, 점프해 올라갈 때와 같이 캐릭터의 상황에 따라 움직임 처리를 다르게 해야한다. 이를 위해 Script는 자신의 현재 행동에 대한 정보를 나타내는 열거체 변수 player_state를 가지고 있으며, FixedUpdate는 해당 변수를 조건으로 사용해 플레이어의 움직임을 처리한다.

85      RaycastHit2D[] hit2D = Physics2D.RaycastAll(transform.position - new Vector3(0.2f, 0.2f, 0.0f), Vector2.right, 0.4f); // 레이캐스트를 통한 지상과의 충돌 체크
        bool ground = false;
        foreach(var hit in hit2D)
        {
            if(hit.collider.CompareTag("Ground"))
            {
                ground = true;
                break;
            }
        }

        if (ground) // 플레이어가 땅에 있는 경우
        {
                Can_Jump = true;
                if(!Input.GetKey(KeyCode.D)) // 점프키 입력 중이 아닌 경우
                {
                    up_time = 0.0f; // 점프 시간 0으로 초기화
                }
        }
        else if (rb.velocity.y < 0.0f && !ground) // 공중에서 떨어지는 중
        {
            Can_Jump = false;
            up_time = up_time_limit + 0.1f; // 현재 점프하는 시간 값을 바꿔 강제로 점프불가능으로 교체
108     }

우선 PlayerController Script의 85번째 줄부터 108번째 줄은 캐릭터가 땅 위에 서있는지 그렇지 않은지 확인하고, 그 상태에 따라 점프 관련 변수를 바꾸도록 하고 있다. 이를 위해 캐릭터 아래에 대해 raycast를 쏘고 Ground Tag를 가진 오브젝트가 검출되는지 확인, 이에 따라 ground 변수의 값을 변경한다.

그 뒤 땅 위에 있으면서 현재 점프 키를 누르지 않으면 점프 시간을 0으로 초기화해 점프를 누르면 그 시간을 잘 반영할 수 있도록 설정하며, 땅 위에 있지 않으면서 떨어지는 경우에는 점프 시간의 값을 크게 설정해 점프가 되지 않도록 설정하였다.

110     if (player_state == current_State.S_standing || player_state == current_State.S_running || player_state == current_State.S_jumping || player_state == current_State.S_jumpswording) // 서있거나, 뛰거나, 점프하거나, 점프 공격중이거나
        {
            if (Can_move) // 움직일 수 있으면
            {
                .....(이동 및 좌우반전 관련)

                if (Can_Jump) // 점프 가능할 때
                {
                    ...... (플레이어 위에 땅이 존재하는지, 아닌지 Raycast를 이용해 확인)

                    if(!not_up && ready_time<=0.0f && rb.mass>=1.0f) // 위가 비어있으면서 플레이어 질량이 1.0이상이면서 준비시간이 0.0 이하이면
                    {
                        if (Input.GetKey(KeyCode.D))
                        {
                            if (up_time < up_time_limit) // 현재 점프 시간이 제한시간을 넘지 않은 경우에 대해
                            {
145~158                       .....(점프 처리 관련)
                            }
                        }
                    } 
                }
            }
        }
166     player_animator.SetBool("Can_jump", ground); // 점프애니메이션을 자연스럽게 만들어줌
    }

캐릭터의 행동 상 움직임 값을 가져도 상관 없는 경우에 대해 110번째 줄과 같이 player_state 변수를 조건으로 이용, 캐릭터의 행동을 읽으며 움직임에 대한 부분을 처리하도록 한다. 이러한 처리 내용으로는 크게 캐릭터 이동(Run 함수), 캐릭터 점프(145~158 Line)이 존재하며, 여기에서도 player_state를 이용해 해당 부분에 있어서 세부적인 처리 (Run함수 내 점프공격과 그렇지 않은 경우를 분리한 부분)가 가능하다.


2. 움직임을 제외한 행동(공격 및 아이템사용)은 어떻게 처리하는가? (PlayerController Script)

플레이어 캐릭터의 공격 및 아이템사용과 같은 부분은 Update 함수를 통해 처리하며 여기에는 플레이어의 무적시간, 행동 쿨타임과 같은 시간 값도 Time.deltaTime을 이용해 처리하고 있다. 또한 Update 내 행동 처리도 FixedUpdate 내 움직임 처리와 같이 player_state를 통해 현재 플레이어의 행동을 읽고, 이를 조건으로 삼아 할 수 있는 행동을 정해주는 방식으로 동작한다.

200     if (player_state == current_State.S_standing || player_state == current_State.S_running) // 뛰거나 서있으면
        {
            if(ready_time>0.0f) // 준비 시간이 있으면 (다른 전투 행동을 할 수 없음)
            {
                ready_time -= Time.deltaTime;
            }
            if(ready_time<=0.0f) // 준비 시간이 없으면
            {
                  ...(커맨드 입력에 따른 행동처리 - Battle_Action 함수 호출)
            }
            if (Input.GetKeyDown(KeyCode.Space)) // 점멸
            {
                if (Rolling == false)
                {
                    Battle_Action("Roll");
                }
            }
        }

예시로 Update내 위 코드와 같이 player_state가 standing, running이면서 캐릭터가 행동을 통한 쿨타임이 없으면 커맨드에 따라 string 인자를 가지는 Battle_Action 함수를 호출, player_state의 상태 및 Animator용 변수를 변경한다. 그리고 이를 통해 Animator는 플레이어의 행동 변화를 animation으로 진행, animation_event를 통해 상세한변화를 실제로 구축한다.

BGM Game

2. 적을 구현하기 위하여

1.부모 스크립트를 통한 틀 만들기 (Enemy Script and Detect_zone Script)

구현해야 하는 적들은 체력 및 플레이어 탐지, 공격받음, 넉백과 같은 공통적으로 공유하는 기능이 존재한다. 따라서 이를 위해 모든 적 개체들이 상속받을 부모 Script를 만들어야 하며, 이를 Enemy Script와 같은 형태로 구현한다.

해당 스크립트에는 위에서 말한 부분을 처리하고자 각 기능을 함수(Damaged_bullet, Damaged_sword, Knockback, detect) 및 변수(detection, hp)로 구현했다.
또한 적들중에서도 크기가 큰 개체, 작은 개체, 원거리공격에 대해 무적인 개체 등 다양한 종류의 개체가 있으며 이에 대한 이펙트 처리를 해야하므로 이를 위한 변수(size_big, size_small)를 설정, 함수에서는 필요에 따라 이를 사용하도록 한다.

또한 적의 탐지 상태도 플레이어를 완전히 탐지했느냐, 탐지했는데 위치를 모르냐, 탐지했고 위치도 알고 있느냐에 따라 행동이 달라져야 한다. 이를 위한 변수 follow_time, detect_time을 설정하며, 변수의 우선순위에 따라 값이 감소하도록 Update코드를 짠다.

또한 탐지를 했는데 위치를 모르면 탐지반경이 넓어져야 하므로 이를 위한 범위 DetectZone을 설정, 이를 위한 Detect Zone을 작성하였다. 해당 Script는 Collider 내 플레이어가 접근하면 Enemy Script의 Detect함수를 실행하여서 적이 다시 플레이어를 탐지하도록 만드는 작업을 실행한다.

위 Enemy Script를 통해 틀을 만들었으면 Grunt_Swordman, Grunt_Archer와 같이 이를 상속받은 Script를 작성, 상세한 행동을 설정하는 것(Raycast를 통한 공격범위 및 공중에 떠있는지 확인, Animator 설정)으로 실제 적 오브젝트를 게임 내에서 구현할 수 있다.

BGM Game

3. 지금 시점에서 프로젝트의 아쉬운 부분은 어디라고 생각하는가? 이를 어떻게 수정해야 좋을까?

코드 내 충돌과 관련된 부분에서 Raycast들은 Raycastall을 이용해 동작하고 있다. 그러나 RaycastAll은 부딪힌 모든 오브젝트를 검출하고 있으며, 오브젝트 수가 많을수록 연산량이 많아진다.

따라서 이를 막기위해 각 오브젝트마다 Tag가 아닌 Layer를 설정하며 이를 Raycast 충돌에 대한 조건으로 주는 방식으로 수정한다면, 연산부분에 있어서 큰 이득을 얻을 수 있을 것이다.

⚠️ **GitHub.com Fallback** ⚠️