To Paint a World

RPG Extreme - 1 본문

3D Engine/Unity

RPG Extreme - 1

Polariche 2025. 4. 7. 20:59

 

https://www.acmicpc.net/problem/17081

백준 (17081) 문제 RPG Extreme 을 Unity 로 구현해보려 한다.

 

 

 

1. Tile Prefabs

먼저 RPG Extreme 에 정의된 타일의 종류는 다음과 같다

  • 주인공 (@)
  • 빈 칸 (.)
  • 벽 (#)
  • 보스가 아닌 몬스터 (&)
  • 보스 몬스터 (M)
  • 가시 함정 (^)
  • 아이템 상자 (B)

이 중, 빈 칸, 벽, 가시 함정, 아이템 상자를 prefab 으로 다음과 같이 만들 수 있다

 

프로토타입용 마테리얼은 유니티 에셋 스토어(https://assetstore.unity.com/packages/2d/textures-materials/gridbox-prototype-materials-129127) 에서 무료로 받을 수 있다.

 

빈 칸

 

 

가시 함정

 

 

아이템 상자

 

 

모든 지형 prefab 에 TerrainComponent 라는 컴포넌트를 만들어 넣어준다.

 

TerrainComponent 의 구성 요소는 다음과 같다.

  • terrain type 의 enum
  • 매 턴마다 플레이어가 밟고 있는 타일이 호출할 Step Event
  • 플레이어가 이동가능한 타일인지 설정하는 CanStep

 

Inspector 로 확인하면 다음과 같이 보인다.

 

 

 

특수 동작이 있는 타일들 (가시 함정, 아이템 상자, 몬스터 등) 에는 동작을 정의하는 별도의 스크립트 (SpikeEvent, BoxEvent 등) 을 부착하여, 이를 StepEvent 에 연결한다.

그렇게 하면 TerrainComponent 의 정의 및 기능과, 개별 타일의 동작이 디커플링될 수 있다

 

 

 

2. Map Generator

 

 

맵 제너레이터 스크립트에선 맵의 크기 (N x M) 와 타일을 넣을 부모 오브젝트 (TilesParent), 타일 종류별 프리팹 리스트를 정의한다.

게임이 시작했을 때, 2중 for문에 의해 타일들이 적절히 배치된다.

    void Start()
    {
        for (int i = 0;i<n;i++)
        {
            for (int j = 0; j<m; j++)
            {
                int tileIdx = Random.Range(0, tilePrefabs.Count);
                GameObject tile = Instantiate(tilePrefabs[tileIdx], new Vector3(i, 0, j), Quaternion.identity, tilesParent.transform);
                tileMap.Add(tile);
            }
        }
    }

 

 

 

3. Player Movement

GameManager 에서 플레이어의 reference 와 위치를 관리한다.

 

플레이어의 이동 구현은 Input 입력을 받아오는 부분과 맵 이동 판정을 판별하는 부분으로 나눈다.

 

Input 입력을 받을 때, 키 값 (up, down, left, right) 에 따라 플레이어가 바라보는 방향을 회전하고, 맵에서의 이동 값을 정하여 실제 이동 함수를 호출한다.

void Update()
    {
        // TODO : move this to somewhere suitable
        // Rotate the player, compute the position difference, and call MovePlayer
        if (Input.GetKeyDown("left") || Input.GetKeyDown("right") || Input.GetKeyDown("up") || Input.GetKeyDown("down"))
        {
            int dx = 0, dy = 0;
            if (Input.GetKeyDown("down"))
            {
                player.transform.rotation = Quaternion.Euler(0.0f, 90.0f, 0.0f);
                dx = 1;
            }
            else if (Input.GetKeyDown("up"))
            {
                player.transform.rotation = Quaternion.Euler(0.0f, 270.0f, 0.0f);
                dx = -1;
            }
            else if (Input.GetKeyDown("left"))
            {
                player.transform.rotation = Quaternion.Euler(0.0f, 180.0f, 0.0f);
                dy = -1;
            }
            else if (Input.GetKeyDown("right"))
            {

                player.transform.rotation = Quaternion.Euler(0.0f, 0.0f, 0.0f);
                dy = 1;
            }
            MovePlayer(dx, dy);
        }
        
    }

 

 

MovePlayer 에서는 플레이어가 이동할 위치를 검증하고, 실제로 이동시킨다.

이동시킨 이후엔 플레이어 발 밑 타일을 작동시킨다.

void MovePlayer(int dx, int dy)
    {
        // See if we can actually move to the target tile

        List<GameObject> tileMap = MapGenerator.singleton.tileMap;
        int n = MapGenerator.singleton.n;
        int m = MapGenerator.singleton.m;

        int nx, ny;

        nx = x + dx;
        ny = y + dy;

        // if the target position is within the range
        if (nx >= 0 && nx < n && ny >= 0 && ny < m)
        {
            GameObject newTile = tileMap[nx * m + ny];

            // if we can actually step onto the tile
            if (newTile.GetComponent<TerrainComponent>().CanStep())
            {
                player.transform.Translate(new Vector3(0.0f, 0.0f, 1.0f));

                x = nx;
                y = ny;

                currentTile = newTile;
            }
        }

        // process the turn
        currentTile.GetComponent<TerrainComponent>().OnStepTurn();
    }

 

 

4. Combat Component

전투 컴포넌트에선 HP, damage, defense 값 등을 정의한다.

    [SerializeField] int maxHP = 20;
    [SerializeField] int curHP = 20;

    [SerializeField] int damage;
    [SerializeField] float damageMultiplier = 1;

    [SerializeField] int defense;
    [SerializeField] float defenseMultiplier = 1;

 

 

플레이어 (혹은 몬스터) 가 데미지를 받으면 OnHit 함수가 호출된다.

OnHit 함수는 공격자의 CombatComponent 내 damage 와 수비자의 defense 를 고려하여 데미지를 계산하고, 현재 HP 에 적용한다.

 

데미지 계산 공식은 공격자의 타입 (가시 함정 / 혹은 몬스터) 에 따라 다르게 적용한다.

   public void OnHit(GameObject hitter)
    {
        CombatComponent combat = hitter.GetComponent<CombatComponent>();

        if (combat == null)
            return;

        int totalDamage;

				// Calculate the damage depending on the type of the hitter
        if (hitter.GetComponent<TerrainComponent>() != null)
        {
            totalDamage = (int) (combat.damage * combat.damageMultiplier);
        } else
        {
            totalDamage = Math.Max(1, (int) (combat.damage * combat.damageMultiplier) - (int) (defense * defenseMultiplier) );
        }

				// apply dmg & Invoke events depending on whether the player has been hurt or healed
        if (totalDamage > 0)
        {
            // damage
            curHP -= totalDamage;
        } else if (totalDamage < 0)
        {
            // heal
            curHP -= totalDamage;
        }
    }

 

 

 

5. UI

HP 표시 슬라이더는 Canvas > Slider 컴포넌트를 통해 간단하게 구현하였다

(참조: https://kimyir.tistory.com/20)

 

 

Canvas 에 UIManager 컴포넌트를 부착하여, slider 의 값을 변경할 수 있도록 하는 함수를 구현한다

public class UIManager : MonoBehaviour
{
    [SerializeField] Slider HPSlider;

    public void ChangeHPBar(int curHP, int maxHP)
    {
        HPSlider.value = (float) curHP / maxHP;
    }
}

 

 

HP 를 실질적으로 변화시키는 OnHit 함수에 ChangeHPBar 를 연결시켜야, HP가 변할 때마다 그 변화가 슬라이더에 반영될 것이다

 

CombatComponent 정의로 돌아가서, UnityEvent<T, T> 를 상속한 HPEvent 클래스를 생성하고 Inspector 에 노출하여, 코드 상에서 다이나믹하게 ChangeHPBar 의 파라미터 (curHP, maxHP) 를 전달할 수 있도록 한다

 

    [Serializable]
    private class HPEvent : UnityEvent<int, int> { }

    [SerializeField] HPEvent onDamagedEvent;
    [SerializeField] HPEvent onHealEvent;

    public void OnHit(GameObject hitter)
    {
		    // ...
		    
        // apply dmg & Invoke events depending on whether the player has been hurt or healed
        if (totalDamage > 0)
        {
            // damage
            curHP -= totalDamage;
            onDamagedEvent.Invoke(curHP, maxHP);

        } else if (totalDamage < 0)
        {
            // heal
            curHP -= totalDamage;
            onHealEvent.Invoke(curHP, maxHP);
        }

    }