일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- VRChat
- 3d 기하학
- bufferobjects
- 수학 #기하학 #벡터 #벡터연산 #선형대수학
- shaders
- vbo
- vao
- NVM
- 쿼터니온
- react
- VRC
- OpenGL
- 회전행렬
- initialization
- kubernetes
- ReactJS
- MongoDB
- bufferswap
- Directx12
- 수학
- ibo
- vertexbufferobject
- BeatSaber
- unity
- liltoon
- callbacks
- indexbufferobject
- LiV
- Drawcall
- vertexattributeobject
- Today
- Total
To Paint a World
RPG Extreme - 1 본문
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);
}
}
'3D Engine > Unity' 카테고리의 다른 글
Elden Ring in Unity - Episode 2 (0) | 2025.03.31 |
---|---|
Elden Ring in Unity - Episode 1 (0) | 2025.03.31 |
VR 게임 아바타 설정 및 녹화 방법 (0) | 2024.12.12 |
Unity 2018.4 버전에서 lilToon 을 임포트하는 방법 (1) | 2024.12.09 |