-
Unity로 제작한 2D 로그라이크 게임입니다.
-
기본적인 역량을 쌓기 위해서 자료구조와 알고리즘 등을 공부하며 제작하기 좋은 장르라고 생각 해 시작하게 되었습니다
-
이전에도 비슷한것을 제작 했으나, 구현방식, 자료구조와 알고리즘 공부를 위해서 공부 후에 다시 제작하며 구현방식을 다르게 했습니다.
- Unity 2022.3.21.f1
- C#
- window 10
-
- 델리게이트나 이벤트 시스템등을 전혀 사용하지않아 사용 해보는 기회가 될것같았습니다.
- 이전에 프로젝트를 만들어 본 결과 데이터 매핑을 하지 않아 A*를 적용하기 힘들었고 자료구조와 알고리즘을 배워서 적용 시킬만한 좋은 실습소재가 될것이라고 생각했습니다.
- 에셋 번들 또한 사용하지 않았었고 포트폴리오이니 공부하면서 적용시켜볼만것들을 공부하고 적용시키고 직접 차이를 보는게 좋았습니다.
- 장비아이템의 착용 여부를 2진수로 판단 하는 등의 거의 코드 전체를 수정하는게 나을것같다는 판단과 반년동안 이전 회사에서 개인적으로 공부한것들을 해볼만한 기회가 된것같아 다시 만들게 되었습니다.
- 재귀와 이중 분할을 이용 한 무작위 맵 생성Assets/Scripts/Managers/Map/MapMake.cs
void MapDivide(int startX, int startY, int endX, int endY, int count)
{//재귀함수로 맵 나누기
if (count < maxCount && endX - startX > 5 && endY - startY > 5)
{
if ((endX - startX) > (endY - startY))
{
count++;
int divided = ((startX + endX) * UnityEngine.Random.Range(dividingMin, dividingMax)) / 200;
divided = RerollDivied(startX, endX, divided);
for (int y = startY; y < endY; y++)
{
Vector2 Pos = new Vector2(divided, y);
TileMap[Pos] = TileType.wall;
}
int door = UnityEngine.Random.Range(startY, endY);
Vector2 doorPos = new Vector2(divided, door);
TileMap[doorPos] = TileType.door;
if (divided - startX > 3)
{
MapDivide(startX, startY, divided, endY, count);
}
if (endX - divided > 3)
{
MapDivide(divided, startY, endX, endY, count);
}
}
else
{
count++;
int divided = ((startY + endY) * UnityEngine.Random.Range(dividingMin, dividingMax)) / 200;
divided = RerollDivied(startY, endY, divided);
for (int x = startX; x < endX; x++)
{
Vector2 pos = new Vector2(x, divided);
TileMap[pos] = TileType.wall;
}
int door = UnityEngine.Random.Range(startX, endX);
Vector2 doorPos = new Vector2(door, divided);
TileMap[doorPos] = TileType.wall;
if (divided - startY > 3)
{
MapDivide(startX, startY, endX, divided, count);
}
if (endY - divided > 3)
{
MapDivide(startX, divided, endX, endY, count);
}
}
}
}
- 플레이어의 이동 전 매핑된 데이터를 바탕으로 해당 포지션의 오브젝트를 판단 후 공격/이동 판정
public void OnkeyPlayerMove()
{
AxisInput();
int nexty = (int)(playerObj.transform.position.y + moveDirection.y);
int nextx = (int)(playerObj.transform.position.x + moveDirection.x);
Vector2 next = new Vector2(nextx, nexty);
if (IsInSize()&&mapScript.TileMap!=null)
{
switch (mapScript.TileMap[next])
{
case TileType.tile:
InputMoveKey();
//Debug.Log("MovableTile");
break;
case TileType.wall:
//Debug.Log("Wall");
break;
case TileType.door:
InputMoveKey();
break;
case TileType.upstair:
InputMoveKey();
break;
case TileType.downstair:
InputMoveKey();
break;
case TileType.monster:
MakeCollider(next);
//MoveState Attack으로 바꾼 후 Attack함수에서 실행 후 idle로 바꿀 필요 있음
break;
case TileType.player:
break;
default:
break;
}
}
}
-
맵 생성시에 맵 데이터를 딕셔너리로 저장 해 미니맵 구현 각 셀에 대한 타일데이터 삽입 후 셀의 색상 변경 -Assets/Scripts/Maps/MiniMap/MiniMapPanel.cs
-
층계에 따라 계산되어 각각의 티어 아이템 확률 차등 적용
-
만들어진 확률에 기반 해 무작위 함수로 몬스터와 아이템 절차적생성
-아이템 생성의 코드 / 장비와 몬스터 또한 비슷한 로직으로 만들어지기 때문에 생략
[Assets/Scripts/Managers/Map/MonsterManager.cs](https://github.com/Althep/FirstProject_Rework/blob/main/Assets/Scripts/Managers/Map/MonsterManager.cs)
-
데이터 저장을 이용한 씬이동으로 층계 구현 방문한 층에는 기존의 데이터를 가져옴 (Json으로 각 데이터의 Text파일 생성 및 저장 / 읽어오기)
-
CSV파일을 Resources.Load 함수를 이용 해 읽어 몬스터/아이템 데이터 적용
주석 처리 된 부분은 직접 짠 코드이나 공백이 생기면 데이터를 잘 읽지 못하는 문제가 생겨 해결하지 못해 인터넷상의 코드 사용
-
몬스터/아이템의 이미지 에셋번들화, 이름에 따라서 해당 스프라이트 적용 번들화 Assets/Editor/AssetBundleBuildManager.cs 스프라이트 적용Assets/Scripts/Managers/Datas/DataManager.cs
public void LoadAssetBundle() { bundlePath = "./AssetBundle/monster"; if(monsterBundle == null) { monsterBundle = AssetBundle.LoadFromFile(bundlePath); } bundlePath = "./AssetBundle/item"; if(itemBundle == null) { itemBundle = AssetBundle.LoadFromFile(bundlePath); } /*bundlePath = "./Bundle/item"; itemBundle = AssetBundle.LoadFromFile(bundlePath);*/ if (monsterBundle == null) { Debug.LogError($"Failed to load AssetBundle {monsterBundle}"); } //monsterBundle.Unload(false); } public Sprite GetItemImage(string imageName) { Texture2D texture = itemBundle.LoadAsset<Texture2D>(imageName); if (texture == null) { Debug.Log("spriteError"); return null; } else { Sprite sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0.5f, 0.5f), texture.height*2); return sprite; } }
-
우선순위 큐와 A*를 이용 해 어느정도까지 플레이어를 추격하는 몬스터 AI
Assets/Scripts/LivingEntity/Monsters/PathFinding.cs ActState에 따라 몬스터의 행동양식을 다르게 함
public void TurnAct() { switch (myState.moveState) { case MoveState.idle: break; case MoveState.move: Move(); break; case MoveState.attack: Attack(); break; default: break; } }
추후에는 각 ActState를 특정 함수를 상속하는 함수로 작성하는게 좋아보임 현재는 각 다른 스크립트를 만들어서 넣었으나 비효율적
- 각 함수명들을 아이템의 정보에 포함, 딕셔너리로 불러오는 기능 ex) 회복포션
public class HealingPotion : ConsumeFunction
{
protected override void ConsumeFunc(LivingEntity entity)
{
if (entity.myState.currntHp + amount > entity.myState.maxHp)
{
entity.myState.currntHp = entity.myState.maxHp;
}
else
{
entity.myState.currntHp += amount;
}
entity.SetHpbarValue();
EventManager.Instance.OnPlayerBattle.Invoke();
}
}
// 각 스탯을 Get, Set함수로 사용 해 Get,Set 함수에 이벤트를 넣는것이 더 좋아보임
- 각 아이템 상속구조를 이용해 고유 필드 구현 ItemBase를 상속하는 ConsumeItem, EquipItem 구현 후 이를 다시 상세히 나누는 방식 (Use 함수를 Override해 장비와 소모품의 기능 차별화)
public class EquipItem : ItemBase { public EquipType _equipType; public EquipOptions options; public EquipItem() { Register(); } protected void Register() { // 아이템 생성 하는 로직에 필요 한 각 Script를 등록 string name = this.GetType().Name; //Debug.Log(name); if (name == "EquipItem") return; if (!GameManager.instance.item.EquipScripts.ContainsKey(name)) { GameManager.instance.item.EquipScripts.Add(name, this); } } public override void Use(LivingEntity entity) { Debug.Log("ItemUse"); if (entity.equips.ContainsKey(_equipType)) { UnEquip(entity); AddValue(entity); } else { AddValue(entity); } } public virtual void AddValue(LivingEntity entity) { } public virtual void UnEquip(LivingEntity entity) { if (entity.equips.ContainsKey(_equipType)) { entity.equips.Remove(_equipType); } } public virtual EquipItem Clone(EquipType type) { // 아이템을 생성 할 때에 스크립트를 넣어주기 위해 해당하는 타입의 스크립트를 클론 해 반환 if (_equipType != type) { _equipType = type; } switch (type) { case EquipType.Weapon: return new Weapon(); //생략 case EquipType.Ring: return new Ring(); } throw new ArgumentException("Type Exception", nameof(type)); } }
4-3 이벤트 관련Assets/Scripts/Managers/EventManager.cs
- 유니티 이벤트를 이용 해 플레이어 레벨업,피격 시 UI갱신
- 유니티 이벤트를 이용 해 지속성 포션 아이템의 기능 구현
public class StrengthPotion : MaintainPotion
{
protected override void ConsumeFunc(LivingEntity entity)
{
maintain = 10;
SetEndTurn();
entity.myState.str += 3;
UnityAction action = () => Action(entity);
EventManager.Instance.OnPlayerMove.AddListener(action);
_cashedAction = action;
}
protected override void Action(LivingEntity entity)
{
if (GameManager.instance.turnManager.turn == endTurn)
{
entity.myState.str -= 3;
Debug.Log("EndAction Actived");
EventManager.Instance.OnPlayerMove.RemoveListener(_cashedAction);
}
}
- 플레이어의 인벤토리 구현, List 이 정보를 바탕으로 인벤토리 UI 구현 Assets/Scripts/UI/Inventory/InventoryUI.cs
- 딕셔너리를 이용 해 플레이어의 장비 확인 (이미 존재하는 키라면 데이터를 교체하는식) Assets/Scripts/Items/Equipment/EquipItem.cs
- 오브젝트의 Layer와 RayCast를 이용 한 전장의 안개(시야시스템)구현 Assets/Scripts/Maps/FogOfWar/FogOfWar.cs
- UI 상속을 통해 위치 지정 및 단축키 할당, 맵핑 Assets/Scripts/UI/UIBase.cs