
GameObject 구현player플레이어 이동의 경우 프레임마다 위치 값을 바꾸는 Translate와 속도를 바꾸는 rigidbody.velocity를 활용하면 아래 나오는 Map Objects와의 상호작용이 제대로 이루어지지 않는 단점이 있었다. 따라서 플레이어의 이동은 AddForce를 활용했다.
Move()플레이어를 이동시킬 때 카메라 기준으로 입력에 따라 움직일 필요가 있었다. 따라서 월드 좌표가 아닌 카메라의 좌표를 기준으로 방향을 잡아야했고, main 카메라를 가져와 방향을 정규화하여 사용했다.
public class PlayerController : MonoBehaviour
{
private void Update()
{
Move();
}
private void Move()
{
float moveX = Input.GetAxis("Horizontal");
float moveZ = Input.GetAxis("Vertical");
// 입력 방향 벡터 정규화
Vector3 front = new Vector3(moveX, 0f, moveZ).normalized;
// 움직임이 0이 아니면
if (front != Vector3.zero)
{
// 카메라 앞 방향
Vector3 camForward = Camera.main.transform.forward;
// 카메라 오른쪽 방향
Vector3 camRight = Camera.main.transform.right;
camForward.y = 0f; // 카메라 y축 고정
camRight.y = 0f; // 카메라 y축 고정
camForward.Normalize(); // 벡터는 고정하고 방향만
camRight.Normalize(); // 벡터는 고정하고 방향만
// 입력값을 카메라 방향 기준으로 변환
Vector3 moveDir = camForward * front.z + camRight * front.x;
rb.AddForce(moveDir * moveSpeed, ForceMode.Force);
}
}
Jump()점프의 경우 isGround 태그를 활용해 땅에 닿았을 때만 점프가 가능하도록 구현했다. 접촉 판단은 OnCollision과 Tag 비교를 활용했다.
public class PlayerController : MonoBehaviour
{
private void Update()
{
// 땅에 붙어있을 때만 점프
if (Input.GetKeyDown(KeyCode.Space) && isGround)
{
Jump();
}
}
private void Jump()
{
rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
}
// 땅에 접촉해 있을 때
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isGround = true;
}
}
// 땅에서 떨어졌을 때
private void OnCollisionExit(Collision collision)
{
if (collision.gameObject.CompareTag("Ground"))
{
isGround = false;
}
}
}
Camera카메라는 플레이어 뒤쪽에 위치하여 마우스 방향에 따라 회전하도록 구현했다. 다만 플레이어를 따라가야해서 플레이어 이동(Update) 이후에 카메라가 이동하도록 LateUpdate에서 Move를 호출해야했다.
public class CameraController : MonoBehaviour
{
// 플레이어가 움직이고 나서 카메라로 확인해야 해서 LateUpdate에서 구현
private void LateUpdate()
{
CameraMove();
}
// 카메라 움직임
private void CameraMove()
{
// 마우스 방향, 플레이어 위치 업데이트
transform.rotation = Quaternion.Euler(curRotationX, curRotationY, 0);
Vector3 movePosition = target.position + transform.rotation * offset;
// 카메라 이동(보간)
transform.position = Vector3.Lerp
(transform.position, movePosition, followSpeed * Time.deltaTime);
}
}
Map1 ObjectsMap1의 컨셉은 장애물을 피하며 골인 지점까지 달리는 것이다. 장애물로 활용한 Object는 총 9개로 3명이 나누어 작업했다.
Tile기본적으로 플레이어가 디디고 있는 바닥 타일을 의미한다. 몇몇 구간의 경우 friction을 조절하여 플레이어가 미끄러지도록 구현했다.
Moving tile타일 위에 올라간 플레이어가 특정방향으로 계속해서 미끄러지는듯한 느낌이 들도록 구현했다. 다만 타일의 모션은 정적이기에 연출을 위해 이후 애니메이션을 추가해야할 것 같다.
private void OnCollisionStay(Collision collision)
{
if (collision.gameObject.layer == _playerLayer)
{
Rigidbody rb = collision.rigidbody;
if (rb != null)
{
Vector3 force = _moveDirection.normalized * _moveForce;
rb.AddForce(force, ForceMode.Force);
}
}
}
Cylinder에셋 중 Dotween을 사용하여 일정 구간을 반복하여 움직이는 원형기둥을 구현했다.
private void Start()
{
transform.DOLocalMoveX(_distance, _duration)
.SetRelative()
.SetEase(_ease)
.SetLoops(-1, LoopType.Yoyo);
}
Hammer에셋 중 Dotween을 사용하여 시계/반시계 방향으로 회전하는 망치를 구현했다.
private void Start()
{
transform.DOLocalRotate(new Vector3(0, _isClockwise == true ? 360 : -360, 0), _duration, RotateMode.FastBeyond360)
.SetEase(Ease.Linear)
.SetLoops(-1, LoopType.Incremental);
}
Pandulum에셋 중 Dotween을 사용하여 공중에서 진자 운동을 하는 오브젝트로 플레이어의 이동을 방해하도록 구현했다.
private void Start()
{
transform.DOLocalRotate(new Vector3(0, 180, 0), _duration, RotateMode.Fast)
.SetEase(_ease)
.SetLoops(-1, LoopType.Yoyo);
}
Square wallCoroutine을 활용하여 앞 뒤로 움직이는 사각 기둥을 구현했다.
private IEnumerator MoveRoutine()
{
float timer = 0f;
while (true)
{
while(timer < _moveTimer)
{
timer += Time.deltaTime;
MoveForward();
yield return null;
}
timer = 0f;
while(timer < _moveTimer)
{
timer += Time.deltaTime;
MoveBack();
yield return null;
}
timer = 0f;
}
}
Bounce block해당 블록에 닿으면 플레이어가 블록면의 수직방향으로 튕겨나가도록 구현했다. 다만 플레이어의 이동 로직에 따라 스크립트를 수정해야할 필요가 있어 추후에 AddForce 방식으로 변경할 예정이다.
public class BounceBlock : MonoBehaviour
{
[SerializeField] private LayerMask playerLayer;
[SerializeField] private float force;
private void OnCollisionEnter(Collision other)
{
Rigidbody rigid = other.gameObject.GetComponent<Rigidbody>();
if (rigid != null)
{
Debug.Log("충돌 진입");
ContactPoint contact = other.contacts[0];
Vector3 forceDir = contact.normal;
Vector3 velocity = rigid.velocity;
Vector3 reflectedVelocity = Vector3.Reflect(velocity,forceDir);
rigid.velocity = reflectedVelocity.normalized * force;
}
}
}
Cannon공중에서 과일에 생성되어 아래로 떨어지도록 구현했다.
public class CannonMovement : MonoBehaviour
{
private IEnumerator FireRoutine()
{
while (true)
{
Fire();
yield return _fireCool;
}
}
private void Fire()
{
// 오브젝트 풀에서 가져오도록 구현
Fruit instance = pool.GetPool();
instance.transform.position = transform.position;
instance.GetComponent<Rigidbody>().velocity = transform.forward * moveSpeed;
}
}
public class Fruit : MonoBehaviour
{
// 일정 시간이 지나면 자동으로 리턴/파괴되도록 구현
private void Update()
{
_timer += Time.deltaTime;
Return();
}
private void Return()
{
if (_timer > liveTime)
{
if (pool == null)
{
Destroy(gameObject);
}
else
{
pool.ReturnPool(this);
}
_timer = 0f;
}
}
}
public class FruitPool : MonoBehaviour
{
// 포탄(과일) 프리팹 지정
[SerializeField] private Fruit fruitPrefab;
// 풀 크기, 5개 이상 배치할 경우 사이즈 늘려야할 필요o
[SerializeField] private int poolSize;
// 오브젝트 풀 Queue로 구현
private Queue<Fruit> _fruits;
private void Awake()
{
Init();
}
/// <summary>
/// 오브젝트 풀에서 포탄(과일)을 불러오는 함수
/// </summary>
/// <returns></returns>
public Fruit GetPool()
{
// 큐에 꺼낼 수 있는 과일이 없는 경우
if (_fruits.Count == 0)
{
// 새로 인스턴스 생성
Fruit instance = Instantiate(fruitPrefab);
return instance;
}
// 큐에서 꺼낼 수 있는 과일이 있는 경우
else
{
// 큐에서 과일을 꺼내 인스턴스 지정
Fruit instance = _fruits.Dequeue();
// 해당 인스턴스 활성화
instance.gameObject.SetActive(true);
return instance;
}
}
public void ReturnPool(Fruit fruit)
{
// 큐에서 꺼내지 않고 새로 생성되어 지정된 pool이 없는 경우
if (fruit.pool == null)
{
// 풀에 반환하지 않고 파괴
Destroy(fruit.gameObject);
}
// 큐에서 꺼낸 경우
else
{
// 해당 인스턴스 비활성화, 이후 풀에 반환
fruit.gameObject.SetActive(false);
_fruits.Enqueue(fruit);
}
}
// 오브젝트 풀 초기화 함수
private void Init()
{
_fruits = new Queue<Fruit>();
for (int i = 0; i < poolSize; i++)
{
Fruit instance = Instantiate(fruitPrefab);
instance.gameObject.SetActive(false);
// 과일의 pool 지정을 여기서
instance.pool = this;
_fruits.Enqueue(instance);
}
}
}
Water점점 위로 차오르는 물을 구현하여 해당 Map의 컨셉이 타임어택이 되도록 만들었다. 플레이어가 차오르는 물에 닿으면 게임 종료가 된다.
private void Move()
{
transform.Translate(Vector3.up * (upSpeed * Time.deltaTime));
}