본문 바로가기

무제_LR

게임 컨셉 결정, 에너지 시스템 추가

게임 컨셉을 고민하느라 시간이 조금 걸리긴 했다만 드디어 방향을 잡았습니다. 와자뵷!

사람이 죽고, 미친 과학자(이 단어부터 식상함을 느낄 수 있겠지만 유달리 표현할 말이 없음)에게 시체를 도둑맞아

뇌가 양분되어 각자 다른 예비 육체에 이식되어 부활합니다.

본체 뇌가 절반, 지속적인 에너지가 필요한 인공 뇌가 절반으로 구성되어 있어 체력 요소에 당위성을 붙일 수 있습니다.

실험(스테이지)에 협력하면 진짜로 부활시켜준다는 조건으로 게임을 시작할 예정입니다.

 

아무래도 그림 실력이 한국 평균보다 월등히 뛰어나다고 말할 수준은 아닌지라 진정성이 와닿지는 않겠지만 나름 괜찮은 시나리오라고 생각합니다.

 

암튼 int형 HP 시스템에서 float형 Energy 시스템으로 변경했습니다.

타격 시 1씩 감소하던 HP 시스템을 지속적으로 감소하는 Energy로 리워크했습니다.

에너지가 고갈될 시 조작이 불가능하고, 양 플레이어 모두 에너지가 고갈되면 스테이지 패배로 이어집니다.

 

  public interface IPlayerEnergyUpdater// 외부에서 UpdateEnergy 호출로 Update 생명주기 따라가기
  {
    public void UpdateEnergy(float deltaTime);

    public void Pause();

    public void Resume();
  }
  
  public interface IPlayerEnergyProvider	//외부에서 상태 체크
  {
    public bool IsInvincible { get; }

    public bool IsDead { get; }

    public bool IsFull { get; }

    public float CurrentEnergy { get; }
  }

  public interface IPlayerEnergyController	//적당한 메소드
  {
    public void Restore(float value);

    public void RestoreFull();

    public void Damage(float value, bool ignoreInvincible = false);

    public void Restart();    
  }

  public interface IPlayerEnergySubscriber	//이벤트 구독
  {
    public enum EventType
    {
      OnRestoreFull,
      OnExhausted,
      OnRevived,
    }
    public void SubscribeEvent(EventType type, UnityAction action);

    public void UnsubscribeEvent(EventType type, UnityAction action);
  }

에너지 관련 인터페이스는 다음과 같습니다.

모두 묶어서 EnergyService라고 이름붙였습니다.

 

  public class PlayerIdleState : IPlayerState
  {
    //생략
    
    public void FixedUpdate()
    {
      moveController.ApplyMoveDeceleration();      
      energyUpdater.UpdateEnergy(UnityEngine.Time.fixedDeltaTime);

      if (reactionController.IsCharging)
        stateController.ChangeState(PlayerStateType.ChargingIdle);
    }
	//생략
}

Update마다 에너지가 닳는건 StateController에서 작동시키게 했습니다.

 

에너지 회복 시스템에는 약간 차별성을 줘봤습니다.

모든 회복 효과는 자신이 아닌 반대편 플레이어가 받도록 만들었습니다.

반쪽이들이 서로 자기가 진짜 몸이라고 투닥투닥 -> 강제 협력 유도라는 시나리오적 당위성도 챙기고

멀티태스킹을 한층 더 깊게 생각하게 하고 제가 생각해도 괜찮을 것 같습니다.

 

public class EnergyItemTriggerPresenter : ITriggerTilePresenter
{
//생략
    private void OnEnter(Collider2D collider2D)//View의 OntriggerEnter2D에서 작동할 예정
    {
      if (collider2D.CompareTag("Player") == false)
        return;	//플레이어 아닌 OnTriggerEnter는 무시

      var playerType = collider2D.GetComponent<IPlayerView>().GetPlayerType();
      var playerPresenter = model.playerGetter.GetPlayer(playerType.ParseOpposite());
//model.playerGetter: 플레이어 Presenter 가져오는 인터페이스
//playerType.ParseOpposite(): Left일 시 Right, Right일 시 Left반환하는 Enum 확장 메소드

      var energyProvider = playerPresenter.GetEnergyProvider();
      if (energyProvider.IsFull)
        return;

      var energyController = playerPresenter.GetEnergyController();
      energyController.Restore(model.data.RestoreValue);

      Deactivate();
    }
    //생략
}

public interface IPlayerGetter
{
  public IPlayerPresenter GetPlayer(PlayerType playerType);

  public bool IsAllPlayerExist();
}

public static class PlayerTypeExtension
{
  public static PlayerType ParseOpposite(this PlayerType playerType)
    => playerType switch
    {
      PlayerType.Left => PlayerType.Right,
      PlayerType.Right => PlayerType.Left,
      _ => throw new System.NotImplementedException(),
    };
}

노란색 배경에 전기 아이콘의 에너지 회복 아이템은 따로 IItem같은걸 만들지 않고 TriggerTile 유형으로 구현했습니다.

 

 

충전 아이템 외 충전소 영역 트리거 타일도 만들어봤습니다.

LineRenderer를 쓸 일이 없었는데 이번 기회에 써먹어보니 재밌네요.

 

충전소 영역에 들어가면 본인의 에너지 고갈이 중단됩니다.

충전소 영역에서 이동 인풋을 입력하면 반대쪽 에너지가 충전되지요.

 

LineRenderer를 사용해 충전 줄? 빨대같은 것도 만들어보았습니다.

 

'충전중임' 상태를 어떻게 표현할까 고민을 조금 했는데, MoveState에 끼워넣을까 말까 하다가 결국 따로 State를 뒀습니다.

플레이어의 State 내부가 아니라 외부에서만 호출되는 BounceState때문에 State 연결에 조금 골머리를 썩였는데

Bounce에서 원 상태로 돌아가는 방향을 Idle로만 잡고,

Idle에서 CharingMove로 직접 연결시키지 않게 하니 나름 깔끔해진 것 같습니다.

 

LineRender의 Position 로직입니다.

설명과 그림이 개떡같다는 의견이 있으시다면 겸허히 받아들이겠습니다.

현재 LineRenderer의 Position들인 Vector3[] previous,

현재 충전소 중심~플레이어 중심의 Position들인 Vector3[] current입니다.

previous는 current를 Lerp하게 따라가는데, 배열의 index에 따라 Lerp의 값이 달라야 합니다.

플레이어에 가까울 수록 더 빠르게 쫓아가고, 플레이어에 멀 수록 천천히 쫓아가 부드러운 곡선처럼 보이게 했습니다.

 

    private async UniTask UpdateLineRendererAsync(
      Transform target,
      CancellationToken token)
    {
      view.lineRenderer.positionCount = LineCount + 1;

      UpdatePositions(view.transform.position, target.position, linePositions);
      view.lineRenderer.SetPositions(linePositions);
		//최초 시작: 충전소 ~ 플레이어 위치 직선으로 세팅
        
      try
      {
        while (true)
        {
          token.ThrowIfCancellationRequested();

          UpdatePositions(view.transform.position, target.position, targetPositions);
			//매 프레임마다 충전소~플레이어 새로운 직선을 가져옴
            
          for (int i = 0; i < LineCount; i++)
          {
            linePositions[i] = Vector3.Lerp(
              linePositions[i],
              targetPositions[i],
              lerpWeights[i] * Time.deltaTime);
          }//각 인덱스마다 lerpWeights에 차등을 둬 직선형으로만 쫓아가지 않도록 만들기

          linePositions[LineCount] = targetPositions[LineCount];

          view.lineRenderer.SetPositions(linePositions);
          await UniTask.Yield(PlayerLoopTiming.Update);
        }
      }
      catch (OperationCanceledException) { }
    }

    private static void UpdatePositions(
      Vector3 begin,
      Vector3 end,
      Vector3[] buffer)
    {
      var direction = (end - begin);
      var step = direction / LineCount;

      for (int i = 0; i <= LineCount; i++)
        buffer[i] = begin + step * i;
    }
  }

 

다음에 할거:

아직 이미지 소스는 없지만 각 플레이어 UI에 포트레잇 배치하기

대화 다이어로그 만들기 및 관련 시스템, 대사 테이블 만들기