Player Condition
플레이어를 설계를 해보자.
먼저 싱글톤으로 전역으로 접근할 CharacterManager를 만들어주자.
여기서 Player를 관리할 것.
using UnityEngine;
public class CharacterManager : MonoBehaviour
{
private static CharacterManager _instance;
public static CharacterManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<CharacterManager>();
if (_instance == null)
{
_instance = new GameObject("CharacterManager").AddComponent<CharacterManager>();
}
}
return _instance;
}
}
private Player _player;
public Player Player
{
get { return _player; }
set { _player = value; }
}
private void Awake()
{
if (_instance == null)
{
_instance= this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
그러고 이제 플레이어와 컨트롤러를 만들어 줄건데, 컨트롤러 부분은 지난번에도 다룬적이 있기에 코드만 보고 넘어가자.
Player
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
public PlayerController controller;
public PlayerCondition condition;
private void Awake()
{
CharacterManager.Instance.Player = this;
controller = GetComponent<PlayerController>();
condition = GetComponent<PlayerCondition>();
}
}
PlayerController
using System;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
private Rigidbody rb;
[Header("Move")]
[SerializeField][Range(1, 10)] private float moveSpeed;
private Vector2 moveInput;
[Header("Look")]
[SerializeField] private Transform camContainer;
[SerializeField] private float lookSensitively;
[SerializeField] private float maxXLook;
[SerializeField] private float minXLook;
private Vector2 lookInput;
private float curCamXRot;
private bool isInvenOpen;
[Header("Jump")]
[SerializeField] private float jumpPower;
[SerializeField] private LayerMask groundLayer;
[SerializeField] private float maxRayDistance;
[Header("Inventory")]
public Action onOpenInventory;
[Header("Interact")]
public Action onInteraction;
private void Awake()
{
rb = GetComponent<Rigidbody>();
Cursor.lockState = CursorLockMode.Locked;
}
private void FixedUpdate()
{
Move();
}
private void LateUpdate()
{
if (isInvenOpen = Cursor.lockState == CursorLockMode.Locked)
{
Look();
}
}
private void Move()
{
Vector3 dir = (transform.forward * moveInput.y + transform.right * moveInput.x).normalized;
dir *= moveSpeed;
dir.y = rb.velocity.y;
rb.velocity = dir;
}
private void Look()
{
curCamXRot += lookInput.y * lookSensitively;
curCamXRot = Mathf.Clamp(curCamXRot, minXLook, maxXLook);
camContainer.localEulerAngles = new Vector3(-curCamXRot, 0, 0);
transform.eulerAngles += new Vector3(0, lookInput.x * lookSensitively, 0);
}
private bool isGrounded()
{
Ray[] rays = new Ray[4]
{
new Ray(transform.position + (transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down),
new Ray(transform.position + (-transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down),
new Ray(transform.position + (transform.right * 0.2f) + (transform.up * 0.01f), Vector3.down),
new Ray(transform.position + (-transform.right * 0.2f) + (transform.up * 0.01f), Vector3.down)
};
for (int i = 0; i < rays.Length; i++)
{
if (Physics.Raycast(rays[i], maxRayDistance, groundLayer))
{
return true;
}
}
return false;
}
public void OnMove(InputAction.CallbackContext context)
{
if (context.phase == InputActionPhase.Performed)
{
moveInput = context.ReadValue<Vector2>();
}
else if (context.phase == InputActionPhase.Canceled)
{
moveInput = Vector2.zero;
rb.velocity = new Vector3(0, rb.velocity.y, 0);
}
}
public void OnLook(InputAction.CallbackContext context)
{
lookInput = context.ReadValue<Vector2>();
}
public void OnJump(InputAction.CallbackContext context)
{
if (context.phase == InputActionPhase.Started && isGrounded())
{
rb.AddForce(Vector2.up * jumpPower, ForceMode.Impulse);
}
}
public void OnInteract(InputAction.CallbackContext context)
{
if (context.phase == InputActionPhase.Started)
{
onInteraction?.Invoke();
}
}
public void OnInventory(InputAction.CallbackContext context)
{
if (context.phase == InputActionPhase.Started)
{
OnToggle();
onOpenInventory?.Invoke();
}
}
private void OnToggle()
{
Cursor.lockState = isInvenOpen ? CursorLockMode.None : CursorLockMode.Locked;
}
}
이제 컨디션을 만들어줄건데 적도 만들예정이니, Condition클래스를 만들고, PlayerCondition에서 Condition에 공통 데이터를 가져와 사용하자.
먼저 Condition이다.
using UnityEngine;
[System.Serializable]
public class Condition
{
public float curValue;
public float passiveValue;
[SerializeField] private float maxValue;
[SerializeField] private float startValue;
public Condition(float hp = 0)
{
maxValue = hp;
startValue = hp;
curValue = startValue;
}
public float GetPercentage()
{
return curValue / maxValue;
}
public void Add(float amount)
{
curValue = Mathf.Min(curValue + amount, maxValue);
}
public void Subtract(float amount)
{
curValue = Mathf.Max(curValue - amount, 0);
}
}
컨디션에 있을 데이터를 생각해봤을 때,
1. 체력
2. 배고픔
3. 스테미나
였고, 위 3개 모두의 공통된 부분을 로직으로 구현하면,
123의 value 감소, 증가, 초기세팅등이 있었다.
이제 PlayerCodition도 생각해보자.
컨디션에서 공통로직은 처리되었으니, Condition타입으로 1,2,3(체,배,스)를 정의해주고, 플레이어에게만 적용되는 추가적인 로직을 구현해보자.
using UnityEngine;
public class PlayerCondition : Unit
{
public UICondition uiCondition;
private Condition mainBoard { get { return uiCondition.mainBoard; } } //health
private Condition memory { get { return uiCondition.memory; } } // hunger
private Condition clock { get { return uiCondition.clock; } } //stamina
[SerializeField] private float noMemoryMainBoardDecay;
private void Update()
{
memory.Subtract(memory.passiveValue * Time.deltaTime);
clock.Add(clock.passiveValue * Time.deltaTime);
if (memory.curValue <= 0)
{
mainBoard.Subtract(noMemoryMainBoardDecay * Time.deltaTime);
}
}
public override void Ondamage(float damage)
{
mainBoard.Subtract(damage);
if (mainBoard.curValue <= 0)
{
Die();
}
}
public void HealMainBoard(float amount)
{
mainBoard.Add(amount);
}
public void HealMemory(float amount)
{
memory.Add(amount);
}
public void Die()
{
Debug.Log("Die");
Time.timeScale = 0;
}
}
메모리는 계속 줄어들고, 스테미나는 계속 오르도록 업데이트에 넣어주었다.
추가적으로, 힐,죽기 기능 메소드도 만들어주었다. ui부분은 이어서 정리하도록 하겠다. 생각보다 구조짜기가 어렵다..