이번주 한 마디
스프린트 2의 들어서고 첫 주.
스프린트 1에서 어느 정도 데모 플레이까지 개발이 되어서 얼마 남지 않았구나 하는 생각이 한순간에 확 사라져 버린 한 주였다.
태스크 쪼개기
스프린트 1에서 가장 많이 느낀 것은 개발 자체 보다도 역할을 나누는 것이었다.
결국 1주 차 마무리 과정에서 강사님과 퍼실님한테 상담을 요청하고 여러 가지 팁을 많이 받게 되었다.
그리고 이번 주는 그 팁을 활용하는 첫 주였다.
- 스프린트 시작 전에 목표를 정하고 목표에 필요한 태스크를 모두 스프린트 2 할 일로 남기기
- 스프린트 시작 전에 태스크들을 묶어서 기능 명세서 미리 작성하기
- 바로 테스트가 가능하게 유닛테스트로 기능을 개발해라
- PR 시간을 준수해라
- 각 테스크에 시간을 1시간 정도 정해두어 1시간이 지나면 다음 날로 미루고 얘기해서 가중치를 정해 마무리해라
그리하여 월요일부터 우리는 배포 전까지 레벨 5까지의 모든 사이클을 개발하겠다는 목표로 태스크를 써보기 시작했다.
- 레벨 1 디자인
- 레벨 2 디자인
- 레벨 3 디자인
- 레벨 4 디자인
- 레벨 5 디자인
- 게임 매니저 클래스에서 다른 매니저를 호출 및 관리
- 사운드 매니저
- 인터렉트 매니저에서 꾸밈 성질의 동작을 순차적으로 실행하도록 만들기
- 인게임 ui
- 게임 설정 ui
- input 키를 담아놓을 상수 클래스 정의
- 키 변경 구현
- 해상도에 따른 ui 변경
- 꾸밈 성질 5개 더 정의하고 구현
- repeat 성질에 대한 검출 구현
...
쓰다가 서로의 눈빛을 보았는데.. 갑자기 숨이 막히는 것 같은 분위기였다.
스프린트 1에서 생각보다 많이 했다고 생각했던 터라 이렇게 개발할 내용들이 많을 것이라고 생각하지 않았기 때문이었다.
그래도 테스크를 나누고 다시 기능명세서를 그 태스크들 중에서 묶이는 것들로 다시 묶어서 정리하였다.
이를테면, 레벨 디자인(레벨 1, 2... 5 디자인), 인게임 ui(인게임 로그, 인게임 설정, 인게임 pause 패널, 레벨 클리어 ui) 같은 식으로 나누는 것이었다.
그러고 나서 그 기능 명세서의 큰 틀을 하나씩 각자 중점으로 개발하기로 한 뒤에 매일 데일리 미팅에서 그 안에 태스크들을 가지고 할 일들을 얘기하도록 했다.
그래서인지 서로 어떤 걸 개발할지 미리 알 수 있었고 어느정도 진행이 되어가는지 알 수 있어서 그 템포에 맞춰 각자 개발 속도를 맞추던가 개발을 도와주는 등 좋은 효과가 있었다.
그래서 한 주가 지난 지금 생각보다 많았던 테스크들 중에서 굵직한 것들은 꽤 많이 해결이 된 것 같다.
다만, 테스크에 시간을 정해두어서 지키자는 규칙이 생각보다 잘 안 지켜져서 pr을 하지 못하는 인원이 종종 생겼다.
이 부분은 고칠 점으로 두어서 다음 주에는 잘 지켜보도록 하였다.
이번 주 나의 트러블 슈팅 - 반복하는 성질 '통통' 구현하기
'통통'은 현재까지 구현하고 있는 성질 중에 유일하게 동작이 실시간으로 반복되고, 특히 위치가 바뀌면서 배열 인덱스도 바뀌다 보니 우여곡절이 많았다.
처음에는 실시간 검출에 초점을 맞춰서 이 repeat 동작들만 raycast, sphere cast 등의 물리적 검출을 하자고 생각했으나 다음과 같은 문제들이 발생했다.
- ray에 빗겨나가게 플레이어가 아래에 서있는 경우
- 검출이 프레임마다 돌면, 계속 ray를 그리므로 상당히 비효율적
- 검출 시점을 잡아도 ray 길이, 오브젝트, 플레이어가 검출 시점 외에 난입 하는 등의 문제에서 완전히 자유로울 수 없음
- 자기 자신도 검출이 됨
- 스케일이 변경된 오브젝트의 어디에서 ray를 발사하거나, spherecast의 반지름 크기를 얼마나 늘릴지에 대해 정확하게 판단하기 어려움
- 플레이어가 아래에서 있다가 나온 경우 다시 새로운 바닥을 찾는데 제대로 검출이 안 되는 현상이 간헐적으로 발생
그러다 생각이 든 것이 해당 오브젝트하고 플레이어만 실시간 이동이 되는 것이므로 이 두 개만 다시 타일맵 기준으로 좌표를 그 시점(위아래 1칸 이동을 마친 직후)에 이동시키면 굳이 ray를 안 쏘고, 배열로 인접 타일을 검출, 비교할 수 있지 않을까 생각이 들어서 그런 식으로 구현했다.
그리고 해당 오브젝트는 위 아래 1칸의 이동을 마친 직후에 그 지점에서 반대로 돌아갈 방향에 인접한 타일 그리고 플레이어가 존재하는지 여부를 먼저 확인하고, 없을 때에 그 방향으로 먼저 배열 인덱스를 이동시키고, 오브젝트를 이동시켰다.
(이동 중간에 오브젝트가 같은 곳으로 이동해서 배열의 값이 겹치는 현상을 방지하기 위함)
가장 중요한 플레이어을 검출하는 것은 플레이어의 위치와 콜라이더 너비로 플레이어가 차지하는 공간을 먼저 계산 후 저장하고 그 배열을 가지고 하였다.
자세히는 점프가 없다는 가정하에 플레이어의 위치 +- 콜라이더의 radius 값을 통해서 플레이어가 차지하는 공간들을 구했고, 최대 4개의 Vector3 배열에 저장했다.
(y가 일정할 때(플레이어의 높이 역시 1이므로)에 정확히 x, z의 꼭짓점에 서면, 4개까지 플레이어가 공간을 차지할 수 있다.)
그리해서 저장한 플레이어의 공간에 만약 bouncy 블럭이 떨어질 지점이 있다면, 혹은 다른 타일이 그 위치에 있다면, 그 지점에서 다시 그 위로 튀도록 하였다.
(타일 검출 로직은 기존에 검출 로직과 마찬가지로 기존 타일맵 배열(3차원 배열)에서 해당 오브젝트의 아래 혹은 위에 게임 오브젝트가 있는지 여부를 확인)
BouncyAdj 소스코드
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.EventSystems;
public class BouncyAdj : MonoBehaviour, IAdjective
{
private EAdjective adjectiveName = EAdjective.Bouncy;
private EAdjectiveType adjectiveType = EAdjectiveType.Repeat;
private int count = 0;
#region 통통 꾸밈 카드 멤버변수
private bool isBouncy; // 꾸밈 부여 여부
float bounceSpeed = 3f; // addValue의 증감 빠르기
float bounciness = 4f; // lerp 이동의 빠르기
int bouncyDir = 1; // 위아래 bouncy 이동 방향
float addValue = 0; // 이동 정도
private Transform player; // 플레이어 transform
private float playerRadius; // 플레이어 capsule collider의 반지름 값
Vector3[] playerTiles; // 플레이어가 차지하고 있는 공간들
#endregion
// todo Test를 위함이 아니라면, 반드시 Start의 내용을 지워주세요
private void Start()
{
// var rb = gameObject.GetComponent<Rigidbody>();
// rb.useGravity = false;
// rb.isKinematic = true;
// player = GameObject.Find("Player").transform;
// playerRadius = player.lossyScale.x * player.GetComponent<CapsuleCollider>().radius;
// StartCoroutine(BounceCoroutine(gameObject));
}
public EAdjective GetAdjectiveName()
{
return adjectiveName;
}
public EAdjectiveType GetAdjectiveType()
{
return adjectiveType;
}
public int GetCount()
{
return count;
}
public void SetCount(int addCount)
{
this.count += addCount;
}
public void Execute(InteractiveObject thisObject)
{
// 즉시 rigidBody을 kinematic 해서 중력 영향 없이 1칸 뛸 수 있도록 만듦
var rb = thisObject.gameObject.GetComponent<Rigidbody>();
rb.useGravity = false;
rb.isKinematic = true;
// thisObject.StartCoroutine(BounceCoroutine(thisObject.gameObject));
InteractionSequencer.GetInstance.CoroutineQueue.Enqueue(BounceCoroutine(thisObject.gameObject));
}
public void Execute(InteractiveObject thisObject, GameObject player)
{
//Debug.Log("Bouncy : this Object -> Player");
}
public void Execute(InteractiveObject thisObject, InteractiveObject otherObject)
{
//Debug.Log("Bouncy : this Object -> other Object");
}
public void Abandon(InteractiveObject thisObject)
{
this.isBouncy = false;
}
public IAdjective DeepCopy()
{
return new BouncyAdj();
}
// 해당 방향에 (몇 간격까지) 블럭이 존재하는지 체크하는 메서드
bool CheckExistBlock(GameObject thisObj, Dir dir, int length = 1)
{
// detectManager의 메서드를 이용해서 해당 방향에 존재하는 타일, 오브젝트를 검출
var checkedObj = DetectManager.GetInstance.GetAdjacentObjectWithDir(thisObj, dir, length);
// null이 아니면, true
// null이면, false
if (checkedObj != null)
{
return true;
}
return false;
}
// 해당 방향에 플레이어가 존재하는지 체크하는 메서드
// 추가로 플레이어가 차지하고 있는 공간을 playerTiles로 저장한다.
bool CheckExistPlayer(GameObject thisObj, Dir dir)
{
// 플레이어의 위치값과 플레이어의 콜라이더 반지름을 가지고,
// x, z축으로 어디서부터 어디까지 차지하고 있는지 min, max값으로 저장
Vector3 playerPos = player.position;
// 타일 배열을 초기화 (플레이어가 차지하는 공간은 최대 4칸 : 점프가 없으므로)
playerTiles = new Vector3[4];
float minX = playerPos.x - playerRadius + 0.5f;
float maxX = playerPos.x + playerRadius + 0.5f;
float minZ = playerPos.z - playerRadius + 0.5f;
float maxZ = playerPos.z + playerRadius + 0.5f;
// 위의 값을 통해서 차지하는 타일을 playerTiles에 저장
int idx = 0;
for (int i = Mathf.FloorToInt(minX); i <= maxX; i++)
{
for (int j = Mathf.FloorToInt(minZ); j <= maxZ; j++)
{
playerTiles[idx] = new Vector3(i, Mathf.FloorToInt(playerPos.y), j);
idx++;
}
}
// 검사할 위치의 좌표를 구함
Vector3 objPos = thisObj.transform.position;
Vector3 targetPos;
// dir값에 따라 오브젝트 위치에서 각 방향의 좌표를 저장
switch (dir)
{
case (Dir.up):
targetPos = Vector3Int.RoundToInt(objPos) + Vector3.up;
break;
case (Dir.down):
targetPos = Vector3Int.RoundToInt(objPos) + Vector3.down;
break;
case (Dir.forward):
targetPos = Vector3Int.RoundToInt(objPos) + Vector3.forward;
break;
case (Dir.back):
targetPos = Vector3Int.RoundToInt(objPos) + Vector3.back;
break;
case (Dir.right):
targetPos = Vector3Int.RoundToInt(objPos) + Vector3.right;
break;
case (Dir.left):
targetPos = Vector3Int.RoundToInt(objPos) + Vector3.left;
break;
// 위치값을 잘못 넣었거나 안 넣으면, 무조건 false
default:
return false;
}
// playerTiles에 해당 좌표값이 있다면, 플레이어가 해당 지점에 있다는 것으로 true
// 없으면, 플레이어가 해당 지점에 없다는 것으로 false
return playerTiles.Contains(targetPos);
}
private void TryBouncy(GameObject obj)
{
// 아래에 무언가가 있는지 체크
if (CheckExistPlayer(obj, Dir.down) || CheckExistBlock(obj, Dir.down))
{
// 위에 오브젝트가 있는지 체크
// 위가 안 막혀있다면...
if (CheckExistPlayer(obj, Dir.up) ? !CheckExistBlock(obj, Dir.up, 2) : !CheckExistBlock(obj, Dir.up))
{
// 위로(bouncyDir = 1) 이동하기
bouncyDir = 1;
// 최대 위로 1칸까지 이동하기 때문에 다시 0으로 초기화
addValue = 0;
// 먼저 배열을 이동시켜서 위로 이동 중에 다른 블럭이 이동해 배열에 두 오브젝트가 겹치는 현상 방지
DetectManager.GetInstance.SwapBlockInMap(obj.transform.position, obj.transform.position + Vector3.up);
}
// 천장이 오브젝트로 막혀있다면...
else
{
// 움직이지 말고 그 상태로 대기
bouncyDir = 0;
// 어차피 0이라 이동이 되지 않고, 언제 위나 아래가 빠질지 모르므로
// addValue를 1로 유지 (1 이상이면 계속 탐색이 들어감)
addValue = 1;
}
}
else
{
// 아래에 아무 것도 없으면, 무조건 아래로 떨어지게 됨
bouncyDir = -1;
addValue = 0;
DetectManager.GetInstance.SwapBlockInMap(obj.transform.position, obj.transform.position + Vector3.down);
}
}
private IEnumerator BounceCoroutine(GameObject obj)
{
// 변수 초기화
addValue = 0;
bouncyDir = 1;
isBouncy = true;
// 처음부터 bounce가 안 되는 상황일 수 있으므로 TryBouncy로 체크 후 bounce 시작
TryBouncy(obj);
// Abandon() 전까지는 계속 bounce를 하게 됨
while (isBouncy)
{
// 위로 혹은 아래로 1칸만큼 도달했을 때에 검출 시작
// (= 배열로 먼저 이동시킨 지점에 도달했을 때)
if (addValue >= 1)
{
// 정확히 1칸을 맞추기 위해서 RoundToInt로 위치 조절
obj.transform.position = Vector3Int.RoundToInt(obj.transform.position);
// 가장 최고점 높이에 도달한 경우에는 플레이어가 들어갈 시간을 잠깐 줌
if (bouncyDir == 1)
{
yield return new WaitForSeconds(0.3f);
}
// 검출 후 bounce 시작
TryBouncy(obj);
}
// 높이를 증감할 value를 시간에 따라 증가 (0 ~ 1)
addValue += Time.deltaTime * bounceSpeed;
//실제로 물체의 포지션을 변경하는 코드
obj.transform.position = Vector3.Lerp(obj.transform.position, obj.transform.position + new Vector3(0, addValue * bouncyDir, 0), Time.deltaTime * bounciness);
// 계속 반복 (repeat Adj)
yield return new WaitForEndOfFrame();
}
}
}
테스트 영상
유데미 코리아 바로가기 :
Udemy Korea - 실용적인 온라인 강의, 글로벌 전문가에게 배워보세요. | Udemy Korea
유데미코리아 AI, 파이썬, 리엑트, 자바, 노션, 디자인, UI, UIX, 기획 등 전문가의 온라인 강의를 제공하고 있습니다.
www.udemykorea.com
💡 본 포스팅은 유데미-웅진씽크빅 취업 부트캠프 유니티 1기 과정 후기로 작성되었습니다.
'Starters 부트캠프 > B - log' 카테고리의 다른 글
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 프로젝트 코스 7주차 학습 일지 (0) | 2023.02.26 |
---|---|
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 프로젝트 코스 6주차 학습 일지 (0) | 2023.02.20 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 프로젝트 코스 4주차 학습 일지 (0) | 2023.02.04 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 프로젝트 코스 3주차 학습 일지 (0) | 2023.01.29 |
유데미 스타터스 유니티 개발자 취업 부트캠프 1기 - 프로젝트 코스 2주차 학습 일지 (0) | 2023.01.22 |