TextMeshPro로 만들어진 문자를 움직이려면 TMP_TextInfo라는 것에 접근을 해서 각 문자의 버텍스 위치를 변경해주면 된다. 나는 Sin파의 절댓값을 사용하여 글자가 움직이게 만들었다.
문제 : Index관련 오류가 떴는데 TextMesh의 버텍스를 불러오지 못하는 문제였다.
원인 : 버텍스는 TMP_Text의 메쉬로부터 불러오는데 메쉬부터가 null이니 버텍스는 당연히 불러오지 못하게 되는 것이다.
해결 : TMP_Text의 ForceMeshUpdate 함수 호출 이후에 text의 mesh를 초기화줌으로써 버텍스를 추적할 수 있게끔 하여 해결하였다.
private void JumpLoadingText()
{
loadingText.ForceMeshUpdate();
_loadingTextMesh = loadingText.mesh;
_verts = _loadingTextMesh.vertices;
for (int i = 0; i < loadingText.textInfo.characterCount; i++)
{
TMP_CharacterInfo c = loadingText.textInfo.characterInfo[i];
int index = c.vertexIndex;
float offset = Mathf.Sin(Time.time + i) * textJumpingHeight;
for (int j = 0; j < 4; j++)
{
_verts[index + j].y += offset;
}
}
_loadingTextMesh.vertices = _verts;
loadingText.canvasRenderer.SetMesh(_loadingTextMesh);
}
오늘의 회고
오늘은 TMP_Text의 글자를 움직이는 법을 배웠다. 일반 텍스트랑은 다르게 버텍스를 조작하여 움직일 수도 있고 컬러도 조정이 가능하다는 점이 쓸만한 것 같다. 유튜브의 여러 컨텐츠에는 다양한 효과들도 있던데 이런 것 하나하나가 게임의 비주얼적인 요소에서 꽤나 좋아보이게 하는 영향을 준다고 생각한다. 다음에는 상자에서 글자를 뽑아내는 것과 같은 효과도 구현해볼 생각이다.
내일은 개인 과제를 제출하는 날이다. 내일까지 열심히 해서 계획한 만큼 결과물을 뽑아내도록 하자. 내일도 파이팅!
저장할 때 Sheet 파일 이름이 중요한데 이 이름이 스크립터블 오브젝트의 리스트의 변수명이 됩니다.
Serialize된 클래스의 변수명은 위의 key 이름과 동일해야한다.
using System;
[Serializable]
public class LunchDialogEntity
{
public int branch;
public string name;
public string dialog;
}
스크립터블 오브젝트에 [ExcelAsset] 속성을 부여하고ScriptableObject의 리스트 변수명과 엑셀 시트의 이름과 동일하게 리스트를 선언해준다. * 만약 ExcelAsset에서 namespace 오류가 뜬다면 아래의 코드를 Packages/manifest.json 안의 "dependency"의 괄호 안의 부분에 넣어준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExcelAsset]
public class LunchDialog : ScriptableObject
{
// 변수명과 엑셀 시트의 이름이 동일해야 함
public List<LunchDialogEntity> Entities;
}
이후에 엑셀 파일을 Reimport 하게되면
스크립터블 오브젝트 파일이 하나 생기게 된다.
이 데이터에 접근하여 text를 불러올 수 있다.
오늘의 회고
오늘은 Excel 파일을 유니티에서 읽어오는 방법을 배웠다. 과정이 귀찮지만 데이터 테이블을 엑셀로 관리해서 불러오는 것은 유용할 것 같다. 맨날 json에 데이터를 적고 불러오는 방식을 사용했는데 다음부턴 엑셀로 불러오도록 해야겠다.
한 번에 한 개의 상태만 가질 수 있고 다른 상태로 전이가 가능한 모델을 FSM(유한 상태 머신)이라고 부른다.
게임에서는 애니메이션 쪽에서 자주 쓰이게 된다. (Idle <-> Walk <-> Run <-> Attack)
이번에 배웠던 예제에서는 신기한 구조를 가지고 있었는데 각 상태들이 new 생성자를 통하여 상태 머신의 인스턴스를 받아온다는 것이었다.
그리고 각 상태는 IState 인터페이스를 상속받으며 ChangeState로상태머신의 IState변수 인스턴스를 교체해준다. (상태 전이)
public class PlayerBaseState : IState
{
protected PlayerStateMachine stateMachine;
protected readonly PlayerGroundData groundData;
public PlayerBaseState(PlayerStateMachine playerStateMachine)
{
stateMachine = playerStateMachine;
groundData = stateMachine.Player.Data.GroundData;
}
}
public class PlayerStateMachine : StateMachine
{
// States
public PlayerIdleState IdleState { get; }
public PlayerWalkState WalkState { get; }
public PlayerRunState RunState { get; }
public PlayerJumpState JumpState { get; }
public PlayerFallState FallState { get; }
public PlayerComboAttackState ComboAttackState { get; }
public PlayerStateMachine(Player player)
{
IdleState = new PlayerIdleState(this);
WalkState = new PlayerWalkState(this);
RunState = new PlayerRunState(this);
JumpState = new PlayerJumpState(this);
FallState = new PlayerFallState(this);
ComboAttackState = new PlayerComboAttackState(this);
}
}
public abstract class StateMachine
{
protected IState currentState;
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState?.Enter();
}
}
오늘의 회고
오늘은 상태 머신에 대해서 배웠다. 어렵지만 그림으로 정리하니 이해하기 좀 더 쉬워진 것 같다. 저 구조 이후에는 BaseState를 상속받은 각 상태에서 조건만 추가하여 다음 상태로 넘어가게 구현하면 된다. 처음 구조짤 때는 어렵지만 그냥 한 곳에서 짜는 것보다 유지 보수하기 쉬워보인다.
문제 : SceneManager.LoadScene으로 게임을 재시작했을 때 static 클래스인 WeaponManager의 다음 판에도 데이터가 계속 남아 있어 문제가 되었다.
public class WeaponManager
{
private static WeaponManager _instance;
public static WeaponManager Instance { get => GetInstance(); }
public List<UpgradeItem> WeaponPrefabs { get; private set; }
public List<WeaponData> WeaponDatas { get; private set; } = new List<WeaponData>();
[Header("Resource Path")]
private const string ITEM_UI_PATH = "UI/Weapons/";
public event Action<WeaponData> WeaponUpgradeEvent;
private static WeaponManager GetInstance()
{
if (_instance == null)
_instance = new WeaponManager();
return _instance;
}
}
원인으로 생각한 것 : SceneManager.LoadScene으로 게임을 다시 시작했을 때 MonoBehaviour를 상속 받은 GameManager 싱글턴의 데이터는 초기화 되는 것으로 보아 싱글턴을 여러 곳에서 관리 하는 것에서 문제가 생긴다고 생각했다.
해결 방안 : WeaponManager의 싱글턴을 제거한 후 GameManager 싱글턴이 WeaponManager까지 관리를 해서 게임을 재시작 할 때 GameManager 싱글턴이 파괴되어 WeaponManager까지 파괴될 수 있도록 했습니다.
public class GameManager : MonoBehaviour
{
public static GameManager instance;
private WeaponManager _weapon;
public WeaponManager Weapon { get => GetInstance(ref _weapon); }
private void Awake()
{
instance = this;
}
public T GetInstance<T>(ref T t) where T : new()
{
if (t == null)
t = new T();
return t;
}
}
오늘의 회고
오늘은 저번 주부터 진행한 팀 프로젝트를 마무리하고 발표하는 날이었다. 추석 연휴가 껴있어서 일정 맞추기도 어렵고 짬짬이 구현을 해야했지만 기한이 다가올수록 팀원들이 많이 도와주어서 급한 순서대로 작업을 잘 쳐낼 수 있었다. 다음엔 리팩토링도 신경써서 잘 해봐야겠다.
그리고 너무 단순하게 한 방향으로만 나가는 것이 재미 없다고 생각해서 두 방향으로 나가게 했다.
360도를 개수만큼 나누어 투사체가 뻗어가게 하였고 다른 방향은 나눈 기준의 절반을 더해줘 두 가지 패턴이 나오게 구현했다.
protected override IEnumerator Shoot()
{
while (shovels.Count < Data.count)
{
Shovel shovel = CreateNewWeapon() as Shovel;
shovels.Add(shovel);
}
currentElapsedTime += Time.deltaTime;
float angleUnit = ANGLE / Data.count;
float angle = 0;
if (isEven)
{
float angleMod = angleUnit * 0.5f;
for (int i = 0; i < Data.count; i++)
{
angle = angleUnit * i + angleMod;
if (angle >= ANGLE)
angle -= ANGLE;
SetShovelState(angle, i);
}
}
else
{
for (int i = 0; i < Data.count; i++)
{
angle = angleUnit * i;
SetShovelState(angle, i);
}
}
isEven = !isEven;
yield return CoroutineRef.GetWaitForSeconds(timePerAttack);
StartCoroutine(Shoot());
}
오늘의 회고
오늘은 무기를 발사하는 Shooter와 발사체를 구현하였다. 최대한 관리 쪽은 Shooter에서 처리하고 발사체의 이동은 따로 Weapon 클래스에서 구현하도록 했다. 구현하면서 구조에 대해서 많이 생각하게 되는데 기본적으로 관리하는 객체와 관리되는 객체의 행동을 구분하는 게 중요하다는 생각이 들었다.
내일부터는 추석 연휴이다. 짬짬이 구현해서 다음주 회의 전까지 맡은 역할을 다 구현하고 합칠 수 있도록 해야겠다. 추석 연휴 잘 보내자!
시도한 것 2 : 리스트를 할당하여 이미 생성한 것이라면 리스트에서 제거하여 중복 검출을 한다.
장점 : 이미 생성한 것 이외의 조건도 추가가 가능하여 유연하다.
private void InstantiateRandomItems()
{
List<UpgradeItem> upgradeItems = new List<UpgradeItem>(WeaponManager.Instance.WeaponPrefabs);
int count = _itemCount;
while (count > 0)
{
int index = UnityEngine.Random.Range(0, upgradeItems.Count);
bool isMax = WeaponManager.Instance.IsLevelMax(upgradeItems[index]);
if (isMax)
{
upgradeItems.Remove(upgradeItems[index]);
continue;
}
Instantiate(upgradeItems[index], weaponParent);
upgradeItems.Remove(upgradeItems[index]);
count--;
}
}
오늘의 회고
오늘은 무기 데이터 구현과 UI 연결을 진행했다. 아직 실제 데이터가 적용(캐릭터의 투사체에 데이터 적용)은 되지 않았지만 내일 실제 데이터와 연결할 생각이다. 그리고 이후에는 각 무기 별로 발사되는 로직을 구현하도록 할 생각이다. 오늘 캐릭터와 연결하는 부분을 고민을 많이 해서 시간이 오래 걸렸지만 그래도 내일 어떻게 구현해야할지 답이 좀 나온 것 같아서 다행이다.
내일은 추석 연휴가 시작되기 전 날인데 열심히 해서 연휴 때 여유롭게 진행했으면 좋겠다. 내일도 파이팅!
나는 Canvas의 SortOrder도 고려하고 싶어서 Stack보다 삽입 삭제가 자유로운 LinkedList를 사용하였고
현재 팝업이 켜져있는지 검출하기 위해 name으로 검사해서 true라면 out으로 반환한 뒤 SortOrder를 적용하게 했다.
여기서 name으로 검출하기 전에 팝업을 생성할 시기에 Instantitate를 사용하면 이름 뒤에 (Clone)이 붙으므로 Substring으로 제거해주는 작업도 진행하였다.
public T ShowPopup<T>() where T : UIBase
{
return ShowPopup(typeof(T).Name) as T;
}
public UIBase ShowPopup(string popupName)
{
if (_currentPopups.Count > 0)
{
if (IsPopupExist(popupName, out UIBase popup))
{
SetPopupFront(popup);
return null;
}
}
if (!_popupDict.ContainsKey(popupName))
{
_popupDict.Add(popupName, ResourceManager.Instance.Load<UIBase>($"UI/{popupName}"));
}
UIBase uiPopup = UnityEngine.Object.Instantiate(_popupDict[popupName]);
uiPopup.name = GetNameSubStringClone(uiPopup.name);
SetPopupFront(uiPopup);
return uiPopup;
}
private bool IsPopupExist(string popupName, out UIBase uiPopup)
{
foreach (var popup in _currentPopups)
{
if (popupName.Equals(popup.name))
{
uiPopup = popup;
return true;
}
}
uiPopup = null;
return false;
}
업그레이드 틀 구현
무기 업그레이드 부분을 맡게 되어 UI도 구현하게 되었다.
오늘의 회고
오늘은 새 프로젝트 발제가 있어 팀 기획을 진행했다. 여러가지 종류 중에 뱀파이버 서바이벌 스타일의 게임을 제작하기로 하였고 나는 UI 팝업과 무기 업그레이드를 맡게 되었다. 원래 맡고 싶었던 부분이던 만큼 꽤 재미있게 진행했고 이후에 레벨업 시 다른 팀원으로부터 호출될 함수도 어떻게 구현할지 구상중이다.
내일은 무기 업그레이드 구체화와 레벨업 관련해서 팀원과 상의할 생각이다. 구현 후 시간이 남으면 다른 작업들을 더 맡아서 할 생각이다. 내일도 파이팅!