진행한 것들

캐릭터 상태머신

아래 글에서 진행한대로 플레이어 애니메이션과 관련된 부분은 상태머신으로 관리하기로 했다. 상태머신은 내가 의도한대로 플레이어가 동작 하도록 하기 위해 적용했다.

https://jcdevelop98.tistory.com/349

 

내일배움캠프 40일차 TIL - FSM

오늘의 학습 키워드 Finite State Machine 공부한 내용 Finite State Machine 한 번에 한 개의 상태만 가질 수 있고 다른 상태로 전이가 가능한 모델을 FSM(유한 상태 머신)이라고 부른다. 게임에서는 애니메

jcdevelop98.tistory.com

 

모든 State는 IState를 상속받고 

public interface IState
{
    void Enter();
    void Exit();
    void UpdateState();
    void PhysicsUpdateState();
    void OnDead();
}

 

StateMachine에서 현재 IState인  _currentState를 ChangeState(IState newState)로 바꿔준다.

public class StateMachine
{
    private IState _currentState;

    public PlayerController PlayerController { get; private set; }

    public PlayerMoveState MovementState { get; private set; }
    public PlayerJumpState JumpState { get; private set; }
    
    public StateMachine(PlayerController playerController)
    {
        PlayerController = playerController;

        MovementState = new PlayerMoveState(this);
    }

    public void Init()
    {
        ChangeState(MovementState);
    }


    public void ChangeState(IState newState)
    {
        _currentState?.Exit();
        _currentState = newState;
        _currentState?.Enter();
    }

    public void Update()
    {
        _currentState.UpdateState();
    }

    public void PhysicsUpdate()
    {
        _currentState.PhysicsUpdateState();
    }
}

 

각 State는 애니메이션을 동작시키는데 예를 들어 PlayerMoveState가 있으면 Enter에서 플레이어의 애니메이션 bool 파라미터를 true로 만들고 Exit에서 false로 만들게끔 했다.

public class PlayerMoveState : PlayerStateBase
{
    public PlayerMoveState(StateMachine stateMachine) : base(stateMachine)
    {
        
    }

    public override void Enter()
    {
        playerController.Animator.SetBool(animationsData.MoveParameterHash, true);
    }

    public override void UpdateState()
    {
        base.UpdateState();
        playerController.Animator.SetFloat(animationsData.SpeedRatioParameterHash, speedRatio);
    }

    public override void Exit()
    {
        playerController.Animator.SetBool(animationsData.MoveParameterHash, false);
    }

    public override void OnDead()
    {
        base.OnDead();
    }
}

 

애니메이션은 블렌드 트리를 사용하였다.

 

 

오늘의 이슈 / 내일 할 것

이슈 : Mixamo에서 가져온 Animation에서 humanoid로 변경 후 Import Message Warning이 뜸

 

시도한 것 : 'Take001'이라는 애니메이션이 문제여서 Animation 탭의 Clips에서 제거함, 그러나 왜인지 Source Take 부분에는 남아 있고 Import Message Warning도 존재함

 

내일 할 것 : 캐릭터 점프, 슬라이딩

 

 

 

오늘의 회고

 오늘은 캐릭터 점프와 슬라이딩 쪽을 구현할 예정이었으나 캐릭터 상태머신과 애니메이션이 더 필요하다고 하여 상태머신 쪽을 먼저 구현하게 되었다. Input을 받아와 이동에 적용하는데 구조를 변경하는 작업이 동반되어서 시간이 좀 걸렸던 것 같다. 리팩토링은 할 땐 귀찮은데 하고 나면 괜히 뿌듯한 작업인 것 같다. 내일도 파이팅!

진행한 것들

캐릭터 이동

캐릭터 이동은 InputController의 MoveAction의 콜백으로 PlayerMovement의 SetDirection을 호출하고 여기서 _direction을 초기화해준다. 이후에 Update에서 프레임마다 호출되는UpdateSpeed로 스피드를 계산해주고 FixedUpdate에서 Move를 호출하여 Rigidbody의 velocity를 세팅하여 적용된다.

- 중력이 필요하기 때문에 Transform으로의 이동 대신 Rigidbody 이동을 선택했다.

- Time.deltaTime이 필요한 계산은 Update로 rigidbody를 사용하는 Move는 FixedUpdate에서 호출하게 했다.

public class InputController : MonoBehaviour
{
    public event Action<Vector2> MoveAction;

    public void OnMove(InputAction.CallbackContext context)
    {
        if (context.canceled)
        {
            CallMoveAction(Vector2.zero);
            return;
        }

        Vector2 inputVec = context.ReadValue<Vector2>();
        // 전달
        CallMoveAction(inputVec);
    }

    private void CallMoveAction(Vector2 inputVec)
    {
        MoveAction?.Invoke(inputVec);
    }
}
[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : MonoBehaviour
{
    private void Update()
    {
        UpdateSpeed();
        Look();
    }

    private void FixedUpdate()
    {
        Move();
    }

    public void SetDirection(Vector2 direction)
    {
        _direction = direction;
    }

    private void UpdateSpeed()
    {
        _speed = _speedCalculator.CalculateSpeed(
            _controller.StatHandler.Data.SpeedMin,
            _controller.StatHandler.Data.SpeedMax,
            _direction == Vector2.zero);
    }

    private void Move()
    {
        _rigid.velocity = new Vector3(_direction.x, _rigid.velocity.y, 0f) * _speed;
    }
}

 

 

캐릭터 회전

구현해야할 것 : 플레이어가 회전할 때 정면을 바라보게끔 구현 (왼쪽에서 오른쪽으로 돌 때는 반시계방향으로, 오른쪽에서 왼쪽으로 돌 때는 시계방향으로)

문제점 : QuaternionLookRotation을 이용해 회전을 구현하면 제한을 걸기가 힘듬. 뒤를 바라보는 경우가 생기거나 문워크를 하는 경우가 생김

private void Look()
    {
        if (_direction == Vector2.zero)
        {
            transform.rotation = Quaternion.LookRotation(_preDirection);
            return;
        }

        _preDirection = (Vector3.right * _direction.x).normalized;

        Quaternion rotation = Quaternion.Slerp(
            transform.rotation, 
            Quaternion.LookRotation(_preDirection), 
            turningSpeed * Time.deltaTime);

        _rigid.velocity = new Vector3(_direction.x, _rigid.velocity.y, 0f) * speed;
        transform.rotation = rotation;
    }

 

해결 방안 : 이해하기 쉬운 Euler각을 이용하여 각도에 제한을 둠, 여기서 Vector3.SignedAngle()을 이용하여 좌/우측 둘 다 고려, Quaternion.RotateTowards는 더 작은 각도인 쪽으로 도는데 이를 이용한 트릭으로 Circle_Angle에(-90도, 90도) 방향으로 1을 더해주거나 빼줌으로써 정면만 바라보게 했음

public Quaternion CalculateRotation(Quaternion rotation, Vector3 _preDirection)
{
    float targetAngle = Vector3.SignedAngle(Vector3.forward, _preDirection, Vector3.up);

    float rotationY = rotation.eulerAngles.y;

    rotationY = targetAngle < 0 ? rotationY++ : rotationY--;

    if (targetAngle < 0)
        targetAngle += CIRCLE_ANGLE;

    Quaternion startRotation = Quaternion.Euler(new Vector3(rotation.x, rotationY, rotation.z));
    Quaternion targetRotation = Quaternion.Euler(new Vector3(rotation.x, targetAngle, rotation.z));

    Quaternion newRotation = Quaternion.RotateTowards(startRotation, targetRotation, _rotationSpeed);

    return newRotation;
}

 

 

 

오늘의 이슈 / 내일 할 것

내일 할 것 : 캐릭터 점프, 슬라이딩, UI 조이스틱 이동 연결

 

오늘의 회고

 오늘은 캐릭터 이동과 회전을 구현하였다. 이동은 비교적 쉽게 구현하였는데 회전은 신경써야할 게 더 많아서 좀 오래 걸렸던 것 같다. 구조에 대해서도 고민을 했는데 프로토타입이니 일단 구현부터 한 뒤에 리팩토링을 한다고 생각하고 진행해야겠다. 내일도 파이팅!

 

 

참고 : 

https://docs.unity3d.com/ScriptReference/Vector3.SignedAngle.html

 

Unity - Scripting API: Vector3.SignedAngle

The angle returned is the angle of rotation from the first vector to the second, when treating these first two vector inputs as directions. These two vectors also define the plane of rotation, meaning they are parallel to the plane. This means the axis of

docs.unity3d.com

 

진행한 것들

구조 설계(진행중)

이 프로젝트에서는 캐릭터, 플레이어, 적에 대한 필수적인 데이터를 스크립터블 오브젝트 형식의 클래스로 만들기로 했다.

우선 스크립터블 오브젝트에서 캐릭터 데이터를 EntityData와 HealthData로 나누었는데 이는 IEntity에서 공격자의 입장과 타겟의 입장을 나누기 위해 두가지로 나누었다. (필요한 데이터만 전달하기 위해)

 

 

에셋 찾기(진행 중)

UI를 역동적으로 보여주기 위해 DoTween이라는 에셋을 사용하기로 했다.

https://assetstore.unity.com/packages/tools/animation/dotween-hotween-v2-27676

 

DOTween (HOTween v2) | 애니메이션 도구 | Unity Asset Store

Use the DOTween (HOTween v2) tool from Demigiant on your next project. Find this & more animation tools on the Unity Asset Store.

assetstore.unity.com

 

오늘의 이슈

 

 

오늘의 회고

 오늘은 프로토 타입을 만들기 위해 UI 스테이지 컨셉을 기획하고 플레이어 데이터를 구상해보았다. 기획한 것들을 UI에 집어넣다보니 조금 UI가 복잡해지는 감이 있지만 일단은 넣고 싶은 것들을 다 넣어놓고 프로토타입에서 테스트를 한 뒤 추가할 것은 추가하고 뺄 건 빼는 방향으로 해볼 생각이다.

 내일은 플레이어 이동을 구현해서 팀원이 만든 맵에 테스트를 해보고 시간이 남으면 조이스틱에 연동까지 할 수 있도록 해야겠다. 내일도 파이팅!

진행한 것들

제목 : 이그라엘

주제 : 로그라이트류 플랫포머 게임

팀 목표 : Android 기기 타겟으로 하여 Google PlayStore에 출시

유니티 버전 : 2022.3.2f1

레퍼런스 : 던전 슬래셔

간단 스토리 : 핵전쟁 이후 세계는 방사능에 오염되었다. 생명공학 연구원 K(가명)는 방사능을 정화하는 나무의 존재를 발견하게 된다. 연구를 거듭하여 방사능을 정화시키자!

 

구조 설계(진행중)

구조 설계에 대해서도 의견을 나눴다.

먼저 필요한 매니저들에 대해 정리를 해보고

 

 

에셋 찾기(진행 중)

에셋은 이번에 무료로 배포한 에셋이 있어서 아포칼립스 분위기에 맞게 파괴된 오브젝트들 위주로 사용하기로 했다.

https://assetstore.unity.com/packages/3d/environments/urban/polygon-battle-royale-low-poly-3d-art-by-synty-128513

 

POLYGON Battle Royale - Low Poly 3D Art by Synty | 3D 도시 | Unity Asset Store

Elevate your workflow with the POLYGON Battle Royale - Low Poly 3D Art by Synty asset from Synty Studios. Find this & other 도시 options on the Unity Asset Store.

assetstore.unity.com

 

스토리 (진행 중)

게임의 배경을 설명하기 위한 스토리도 만들고 있다.

 

오늘의 이슈

 

오늘의 회고

 오늘은 최종 프로젝트 기획을 본격적으로 시작했다. 우리 조는 기획이 개발 전반적으로 영향을 끼치는 것을 알고 있기 때문에 기획에 시간을 충분히 쏟고 시작하기로 했다. 먼저 우리는 출시가 목표이기 때문에 레퍼런스 게임을 잡고 우리만의 컨셉(시스템)을 넣어서 새로운 로그라이크 게임을 만들기로 했다. 그리고 필수적인 기능들이 뭔지 미리 생각해보고 클래스를 만들어보기도 했다.

 내일은 오늘 한 것에 이어서 기획을 더 해볼건데 에셋과 더불어 스테이지 컨셉을 정할 생각이다. 내일도 파이팅!

공부한 내용

프로젝트 마무리

최종 프로젝트 진행 전 팀 프로젝트를 마무리 했다.

각자의 미니게임을 합치는 부분에서 좀 오래 걸렸고 특히나 합칠 때 버그를 잡는 부분에서 시간이 많이 걸렸던 것 같다.

특히나 마지막에는 프로젝트 기한을 맞추다보니 확장성이 부족한 코드들을 작성해서 불편한 기분을 느꼈다.

 

오늘의 회고

 오늘은 최종 프로젝트 전 팀 프로젝트 발표가 있었다. 오전 중에는 발표 영상 만드느랴 버그 잡느랴 정신없이 지나갔고 오후에는 팀 발표물들을 보면서 다른 팀에게서도 배울 점이 많다고 생각이 들었다. 최종 프로젝트에는 기존에 프로젝트를 하면서 아쉬운 점들을 모두 보완하도록 노력해야겠다. 다음 주 최종 프로젝트 화이팅!

공부한 내용

시간증가에 따른 등급 감소

내 게임의 규칙은 빠른 시간 안에 정상에 오르는 것인데 시간이 증가할 수록 등급이 감소해야하는 로직을 구현해야했다.

스크립터블 오브젝트에서 기준을 정하고

현재 점수(시간) - 기준 >= 0 의 조건으로 결과를 도출해낼 수 있었다.

int integerTime = (int)elapsedTime;
string grade = gradeCalculator.CalculateGrade(integerTime, out int gold, out int reverseScore);
public string CalculateGrade(int score, out int gold, out int reverseScore)
{
    string grade = string.Empty;
    gold = 0;
    reverseScore = 0;

    for (int i = 0; i < grades.Length; i++)
    {
        if (score - data.ScoreCriteria[i] <= 0)
        {
            grade = grades[i];
            gold = golds[i];
            reverseScore = data.ScoreCriteria[grades.Length - 1 - i];
            break;
        }
    }

    return grade;
}

 

 

오늘의 회고

 오늘은 내일 프로젝트 제출을 위해 엔딩 씬이든 결과 연결이든 여러가지 작업을 진행했다. 늦은 시간까지 다들 열심히 하니 나도 더 열심히 하게 되는 것 같다. 내일 최소한 버그가 없이 제출하는 것이 목표인데 다들 열심히 해주시니 가능하다고 생각한다. 내일도 파이팅!

오늘의 학습 키워드

Async와 Coroutine

 

공부한 내용

Async와 Coroutine

프로젝트를 진행하면서

캐릭터가 오브젝트에 부딪혔을 때

1. 캐릭터의 조작을 멈추고

2. 일정 시간이 지나고 난 후 풀어주는 로직을 짜기로 했다.

3. 이 때 캐릭터에 다시 충돌이 들어오면 경과 시간을 초기화해주는 조건이 필요했다.

4. 이를 기다리는데 다른 로직이 방해받지 않고 기다렸으면 좋겠다고 생각하고 정보들을 찾아보기로 했다.

 

찾아보니 Unite Copenhagen에서 진행한 자료가 있어서 참고해봤다.

대충 이런 차이점이 있는데 내 경우에서는 MonoBehaviour를 상속받지 않으므로 Async를 사용하기로 했다.

 

다음은 Async를 적용하여 MonoBehaviour를 상속받는 Obstacle 클래스에서 OnCollisionEnter 이벤트가 호출되면 조건을 체크하여 ObstacleCollision의 ApplyCollision을 호출하고 물리적 힘을 준 뒤 캐릭터가 이미 충돌 상태인지 확인하고 처리해주는 것을 구현한 로직이다.

public class Obstacle : MonoBehaviour, IPoolingObject<Obstacle>
{
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.collider.TryGetComponent(out CharacterController controller))
        {
            _obstacleCollision.ApplyCollision(controller, collision.rigidbody, -collision.GetContact(0).normal, mass, waitMilliseconds);
        }
    }
}
public class ObstacleCollision
{
    public async void ApplyCollision(CharacterController controller, Rigidbody oppositeRigid, Vector3 towardTargetVec, float mass, int waitMilliseconds)
    {
        oppositeRigid.AddForce(towardTargetVec * mass, ForceMode.Impulse);

        controller.SetImmovable();

        await Task.Delay(waitMilliseconds);

        controller.SetMovable();
    }
}
public class CharacterController : MonoBehaviour
{
    public void SetMovable()
    {
        if (_collisionStack > 0)
        {
            _collisionStack--;

            if (_collisionStack == 0)
                _canMove = true;
        }
    }

    public void SetImmovable()
    {
        _collisionStack++;

        if (_collisionStack == 1)
            _canMove = false;
    }
}

 

 

오늘의 회고

 오늘은 코루틴과 비동기의 차이점에 대해서 배웠다. 코루틴은 유니티에서 MonoBehaviour를 상속 받을 때 싱글 쓰레드로 동작하는 것이고 Async는 멀티 쓰레드로 웹에 요청을 보내거나 파일 입출력을 할 때 많이 쓰인다는 것을 배우게 되었다.

 내일은 게임의 결과를 보여주는 창과 SFX 관리를 할 생각이다. 내일도 열심히 하자 파이팅!

 

 

참고 : 

https://www.youtube.com/watch?v=7eKi6NKri6I

 

오늘의 학습 키워드

단일 책임 원칙

 

공부한 내용

단일 책임 원칙

MonoBehviour을 상속 받고 충돌을 체크하며 float의 mass 값을 가진 Obstacle이 있고

충돌체에 물리적인 힘을 가하는 ObstacleCollision이 있다.

ObstacleCollision은 Obstacle 클래스의 OnCollisionEnter로부터 필요한 정보(Rigidbody, normal, mass) 값만 받아와서 충돌을 처리한다. -> 역할 구분이 확실해진다.

public class Obstacle : MonoBehaviour
{
    [Header("Collision")]
    [SerializeField] private float mass;
    private ObstacleCollision _obstacleCollision;
    
    private void Awake()
    {
        _obstacleCollision = new ObstacleCollision();
    }
    
    private void OnCollisionEnter(Collision collision)
    {
        if (collision.rigidbody != null)
        {
            _obstacleCollision.ApplyCollision(collision.rigidbody, -collision.GetContact(0).normal, mass);
        }
    }
}
public class ObstacleCollision
{
    public void ApplyCollision(Rigidbody oppositeRigid, Vector3 towardTargetVec, float mass)
    {
        oppositeRigid.AddForce(towardTargetVec * mass, ForceMode.Impulse);
    }
}

 

 

 

오늘의 회고

 오늘은 Solid 원칙 중 단일 책임 원칙을 보고 클래스가 하는 역할에 따라 구분해보았다. Obstacle의 클래스의 다른 부분이 얼마나 수정되든 ObstacleCollision의 클래스는 수정할 필요가 없고 그 반대도 동일하다. 이래서 단일 책임 원칙을 사용하나 싶다.

 내일은 오브젝트 풀링을 이용해 오브젝트들을 세 곳에서 생성하고 플레이어가 경사진 다리를 올라가게 할 것이다. 내일도 열심히 해보자 파이팅!

 

 

참고 : 

https://www.youtube.com/watch?v=Eyr7_l5NMds&list=PLB5_EOMkLx_WjcjrsGUXq9wpTib3NCuqg

 

오늘의 학습 키워드

Fog

 

공부한 내용

Fog

포스트 프로세싱을 이용한 Fog 효과가 아닌 간단한 Material을 이용해 Fog를 사용하는 것성능상 유리하다고 하여 적용해보기로 했다.

1. Material을 만들고 Shader를 Particles/Standard Unlit으로 만들어준 뒤

2. Rendering Mode를 Transparent 또는 Fade로 적용한다.

3. Soft Particles를 체크하고 Near fade와 Far fade 값을 적절히 조정해준다.

4. Albedo값을 원하는 색과 알파 값을 정해주면 간단한 Fog Material이 만들어진다.

* 만약 Soft Particles가 적용이 안 된다면 (한 톤으로만 적용된다면) Quality의 Soft Particles를 체크해주면 된다.

이후에 Plane에 Fog Material을 적용해주면

이렇게 간단한 Fog 효과를 만들 수 있다.

 

 

오늘의 회고

 오늘은 Particle Shader를 이용하여 간단한 Fog 효과를 만들어봤다. 정말 간단하고 쉽지만 성능도 포스트 프로세싱보다 쓸만하고 보기에도 괜찮은 게 모바일에서 유용하게 사용할 수 있을 것 같다. 오늘 고정 오브젝트 배치 등 전체적인 틀을 만들어놨으니 내일부터 본격적으로 플레이어 충돌이나 오브젝트 생성 등 핵심 로직을 구현할 생각이다.

 오늘 시간이 없어서 코드 카타 문제를 풀지 못했는데 내일 그 문제를 격파해야겠다. 내일도 파이팅!

와이어 프레임

이번 팀 프로젝트는 미니 게임을 모아서 하나의 게임을 만드는 프로젝트이다.

여러가지 자신이 하고싶은 종류의 미니 게임을 나열한 뒤 그 중에서 제일 하고 싶은 게임을 선택해서 만드는 방식이다.

컨셉은 전역을 하기 위해서 미니게임 수행도가 일정 기준치를 넘어야 클리어가 되는 컨셉이다.

입대 이미지는 달리2를 이용하여 가져왔다.

 

 

오늘의 회고

 오늘은 새 팀으로 프로젝트를 기획했다. 운 좋게 Synty 스튜디오에서 에셋을 무료로 할인하고 있어서 팀원이 모두 구매를 한 뒤 사용하기로 했다. 튜터님이 커뮤니티에 올려주셔서 알게 된 내용이었다. Dalle-2 이미지는 살짝 깨져있거나 어색한 부분이 존재하는데 우리나라 군부대의 정보가 얼마 없어서 그렇지 않나 싶다.

 다음주는 미니게임을 만들 예정이다. 나는 장애물 피하기를 만들건데 경사진 면에 장애물이 떨어지고 플레이어는 이 장애물들을 피해서 올라가는 게임이다. 다음주도 잘해보자 파이팅!

+ Recent posts