오늘의 학습 키워드

TextMeshPro 움직이기

 

공부한 내용

TextMeshPro 움직이기

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의 글자를 움직이는 법을 배웠다. 일반 텍스트랑은 다르게 버텍스를 조작하여 움직일 수도 있고 컬러도 조정이 가능하다는 점이 쓸만한 것 같다. 유튜브의 여러 컨텐츠에는 다양한 효과들도 있던데 이런 것 하나하나가 게임의 비주얼적인 요소에서 꽤나 좋아보이게 하는 영향을 준다고 생각한다. 다음에는 상자에서 글자를 뽑아내는 것과 같은 효과도 구현해볼 생각이다.

 내일은 개인 과제를 제출하는 날이다. 내일까지 열심히 해서 계획한 만큼 결과물을 뽑아내도록 하자. 내일도 파이팅!

오늘의 학습 키워드

ManiFest 오류

 

공부한 내용

UnExpected Token Manifest 오류

Git에 업로드를 하던 중 UnExpected Token이라면서 Manifest 충돌에서 오류가 생겼다.

 

원인이라고 생각했던 것 : mainfest 파일에서 충돌이 일어났으니 manifest 파일을 고치면 된다고 생각했다.

결과 : 반은 맞았다. manifest 파일 쪽에서 충돌이 일어난 것이 맞았고 수정을 하게 되었지만 아직도 문제가 완전히 해결되지는 않았다.

 

다른 원인 :  Packages 폴더에는 manifest 파일 말고 packages.lock.json이라는 파일도 있는데 이 부분에서도 충돌이 일어났던 상황이었다.

 

결과 : 잘 작동하게 되었다.

 

오늘의 회고

 오늘은 DoTween을 이용해서 타이틀 씬과 로딩 씬을 구현했다. 처음 써보는 기능인데 내가 직접 Coroutine으로 효과를 구현하는 것보다 쉽고 다양한 것들이 존재했다. 현업에서도 쓰인다고 하니 앞으로도 계속 애용해야겠다.

 내일은 상태머신을 이용해 캐릭터를 움직이게끔 만들려고 한다. 저번에 배웠던 것을 실제로 적용해보는 시간을 가질 것이다. 내일도 파이팅!

오늘의 학습 키워드

Excel에서 데이터 읽어오기

 

공부한 내용

Excel에서 데이터 읽어오기

엑셀에서 첫 줄에 key 값을 적고 그 아래로 value들을 적는다.

저장할 때 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"의 괄호 안의 부분에 넣어준다.

"net.mikinya.unity-excel-importer": "https://github.com/mikito/unity-excel-importer.git?path=Assets/ExcelImporter#v0.1.1/upm",
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[ExcelAsset]
public class LunchDialog : ScriptableObject
{
    // 변수명과 엑셀 시트의 이름이 동일해야 함
    public List<LunchDialogEntity> Entities;
}

이후에 엑셀 파일을 Reimport 하게되면 

스크립터블 오브젝트 파일이 하나 생기게 된다.

이 데이터에 접근하여 text를 불러올 수 있다.

 

 

오늘의 회고

 오늘은 Excel 파일을 유니티에서 읽어오는 방법을 배웠다. 과정이 귀찮지만 데이터 테이블을 엑셀로 관리해서 불러오는 것은 유용할 것 같다. 맨날 json에 데이터를 적고 불러오는 방식을 사용했는데 다음부턴 엑셀로 불러오도록 해야겠다.

 내일부터는 개인 과제 개발에 들어가는데 기획한대로 잘 구현했으면 좋겠다. 내일도 파이팅!

 

오늘의 학습 키워드

Finite State Machine

 

공부한 내용

Finite State Machine

한 번에 한 개의 상태만 가질 수 있고 다른 상태로 전이가 가능한 모델을 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 interface IState
{
    void Enter();
    void Exit();
    void handleInput();
    void Update();
    void PhysicsUpdate();
}
public abstract class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState newState)
    {
        currentState?.Exit();

        currentState = newState;

        currentState?.Enter();
    }
}

 

 

오늘의 회고

 오늘은 상태 머신에 대해서 배웠다. 어렵지만 그림으로 정리하니 이해하기 좀 더 쉬워진 것 같다. 저 구조 이후에는 BaseState를 상속받은 각 상태에서 조건만 추가하여 다음 상태로 넘어가게 구현하면 된다. 처음 구조짤 때는 어렵지만 그냥 한 곳에서 짜는 것보다 유지 보수하기 쉬워보인다.

 내일은 개인 과제로 무엇을 만들지 기획을 좀 해봐야겠다. 내일도 파이팅!

오늘의 학습 키워드

static 클래스

 

공부한 내용

MonoBehaviour를 상속받은 static 클래스 vs static 클래스

문제 : 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;
    }
}

 

 

오늘의 회고

 오늘은 저번 주부터 진행한 팀 프로젝트를 마무리하고 발표하는 날이었다. 추석 연휴가 껴있어서 일정 맞추기도 어렵고 짬짬이 구현을 해야했지만 기한이 다가올수록 팀원들이 많이 도와주어서 급한 순서대로 작업을 잘 쳐낼 수 있었다. 다음엔 리팩토링도 신경써서 잘 해봐야겠다.

 내일은 새로운 것을 공부할텐데 기대가 된다. 열심히 잘 해봐야겠다. 내일도 파이팅!

오늘의 학습 키워드

Sin, Cos

 

공부한 내용

쇠스랑 무기 구현

쇠스랑 무기는

1. 마우스의 위치로 바라보는 방향이 정해지며

2. 그 방향대로 직선으로 갔다가 돌아오는 움직임으로 구현하도록 했다.

마우스 위치에서 내 위치에를 뺀 targetRot을 구해 transform.up = targetRot에서 바라보는 방향을 정해주고

첫 번째 while에서 마우스 위치로 가는 것, 두 번째 while에서 offset위치로 돌아오도록 만들었다.

protected override IEnumerator Move()
    {
        Vector2 targetPos = _mainCam.ScreenToWorldPoint(Input.mousePosition);
        float harfDuration = movingDuration * 0.5f;
        float elapsedTime = 0f;
        float normalizedRatio = 0f;
        Vector2 targetRot = (targetPos - (Vector2)transform.position).normalized;

        transform.up = targetRot;

        while (elapsedTime < harfDuration)
        {
            elapsedTime += Time.deltaTime;
            normalizedRatio = elapsedTime / harfDuration;
            transform.position = Vector2.Lerp(transform.position, targetPos, normalizedRatio);
            yield return null;
        }

        elapsedTime = 0f;

        while (elapsedTime < harfDuration)
        {
            elapsedTime += Time.deltaTime;
            normalizedRatio = elapsedTime / harfDuration;
            transform.position = Vector2.Lerp(transform.position, offsetTransform.position, normalizedRatio);
            yield return null;
        }

        StartCoroutine(Move());
    }

 

 

낫 무기 구현

낫 무기는

1. 캐릭터를 중심으로 원형으로 돌며

2. 자신을 중심으로 원형으로 회전하도록 구현하도록 했다.

transform.rotation에 Quaternion.Euler로 자기 자신의 회전을 정해주고

단위원에서의 삼각함수를 이용해 x,y의 위치를 정해주었다.

protected override IEnumerator Move()
    {
        transform.rotation = Quaternion.Euler(0, 0, -currentAngle);

        if (currentAngle < 360f)
        {
            float xPos = Mathf.Sin(currentAngle * Mathf.Deg2Rad);
            float yPos = Mathf.Cos(currentAngle * Mathf.Deg2Rad);

            transform.position = new Vector2(xPos, yPos) * radius;
            currentAngle += Time.deltaTime * moveSpeed;

            yield return null;
        }
        else
            currentAngle = 0f;

        StartCoroutine(Move());
    }

 

결과

 

 

오늘의 회고

 오늘은 무기 공격 방식과 업그레이드 부분을 구현했다. 다음에는 실제 몬스터에게 데미지를 입힐 수 있도록 Collider 충돌 로직을 구현해야겠다. 연휴라도 짬짬이 시간을 내서 씬을 합치는 날인 월요일까지 맡은 부분을 완성할 수 있도록 해야겠다. 월요일까지 화이팅!

공부한 내용

원형으로 발사되는 투사체 구현

플레이어를 중심으로 특정 갯수만큼 원형으로 발사되는 투사체를 구현하고 싶었다.

그리고 너무 단순하게 한 방향으로만 나가는 것이 재미 없다고 생각해서 두 방향으로 나가게 했다.

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 클래스에서 구현하도록 했다. 구현하면서 구조에 대해서 많이 생각하게 되는데 기본적으로 관리하는 객체와 관리되는 객체의 행동을 구분하는 게 중요하다는 생각이 들었다.

 내일부터는 추석 연휴이다. 짬짬이 구현해서 다음주 회의 전까지 맡은 역할을 다 구현하고 합칠 수 있도록 해야겠다. 추석 연휴 잘 보내자!

 

오늘의 학습 키워드

UI 중복 없이 랜덤 뽑기

 

공부한 내용

UI 중복 없이 랜덤 뽑기

구현해야할 것 : UI 안에 아이템을 등록해야 하는데 중복 없이 뽑으려고 한다.

시도한 것 1 : 배열을 검사하여 이미 생성했다면 continue로 다시 돌아가 반복하기

문제점 : 이미 검사한 것을 반복적으로 검사하게 되어 시도 횟수가 많아질 수 밖에 없다.

시도한 것 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 연결을 진행했다. 아직 실제 데이터가 적용(캐릭터의 투사체에 데이터 적용)은 되지 않았지만 내일 실제 데이터와 연결할 생각이다. 그리고 이후에는 각 무기 별로 발사되는 로직을 구현하도록 할 생각이다. 오늘 캐릭터와 연결하는 부분을 고민을 많이 해서 시간이 오래 걸렸지만 그래도 내일 어떻게 구현해야할지 답이 좀 나온 것 같아서 다행이다.

 내일은 추석 연휴가 시작되기 전 날인데 열심히 해서 연휴 때 여유롭게 진행했으면 좋겠다. 내일도 파이팅!

오늘의 학습 키워드

UI 팝업 관리

 

공부한 내용

UI 팝업 관리

지난 주에 계획했던 대로 UI 팝업 관리를 구현하게 되었다.

ShowPopup에서 현재 켜져있는 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 팝업과 무기 업그레이드를 맡게 되었다. 원래 맡고 싶었던 부분이던 만큼 꽤 재미있게 진행했고 이후에 레벨업 시 다른 팀원으로부터 호출될 함수도 어떻게 구현할지 구상중이다.

 내일은 무기 업그레이드 구체화와 레벨업 관련해서 팀원과 상의할 생각이다. 구현 후 시간이 남으면 다른 작업들을 더 맡아서 할 생각이다. 내일도 파이팅!

공부한 내용

Status와 장착 UI를 연동

아이템을 장착할 때 Status에 적용이 되도록 구현하였습니다.

 

테스트 영상

3일간 진행했던 기능 테스트입니다.

 

오늘의 회고

  오늘은 3일간 진행했던 과제를 제출했다. 오전에 버그도 잡고 Status와 장비 장착을 연동하기도 했다. 항상 제출 기간이 가까워질수록 더 집중이 잘 되는 것 같다. 부족한 부분도 많았지만 팀 프로젝트 맡을 때는 그 부분들을 보완해서 적용하도록 역할을 나눌 때 적극적으로 어필해야겠다.

 다음 주에는 팀 프로젝트 발제가 있다. 팀 프로젝트에서 이번 주에 아쉬웠던 점들을 모두 보완하도록 하자. 다음 주도 파이팅!

+ Recent posts