-
Notifications
You must be signed in to change notification settings - Fork 5
[Sprint 2] 11‐18 회고 회의
백수지 — Sprint2의 결과로 플레이할 수 있는 무언가(튜토리얼)를 제작할 수 있었다는 데에 의의가 있는 것 같다. 초기 계획에는 Sprint3, 4의 태스크로 예정돼있던 기능들도 Sprint2에서 일부 추가했기 때문에 이후 단계에서 코드 리팩토링, 시나리오 테스트, Github Action 등을 이용한 버그 테스트, 기능 확장을 하는 데에 시간을 더 쓸 수 있을 것이라 예상한다.
오제헌 — 튜토리얼이 짧다고 생각할 수 있지만 튜토리얼에서 적용한 에셋, 스크립트들을 확장한다면 다음 스테이지들을 더 빠르게 만들 수 있을 것이다. 튜토리얼에는 들어가지 않은 에셋도 있고 다음 퍼즐도 구상되어 있어 다음 스프린트가 더 기대된다.
윤승혁 — 현재 스프린트까지 플레이할 수 있는 뚜렷한 결과물이 나왔다. 현재 페이스대로 진행한다면 좋을 것이라고 생각한다.
조휘현 — 플레이할 수 있는 스테이지가 나왔고 다음 스테이지들은 배치만 하면 비슷하게 실행 할 수 있어서 의미가 있었던 스프린트라고 생각한다.
잘 된 부분
백수지 — 슬랙 혹은 중간 회의를 통해 에셋 리뷰, 코드 논의가 원활히 이루어져 팀이 원하는 방향성을 서로 빠르게 반영할 수 있었다. 코드에 대해서는, 스프린트2의 초기 계획 범위 이상으로 카메라, 플레이어 움직임 스크립트를 확장해주셔서 실제 스테이지를 유추할 수 있는 튜토리얼 플레이가 가능해졌다. 에셋에 대해서는, Sprint2에서는 블렌더에서 작업한 내용을 유니티에서 반드시 테스트해 이상 현상을 모두 해결한 후 push할 수 있도록 작업해 유니티 에셋 에러를 최소화할 수 있었다.
오제헌 — 무언가 잘 되지 않는 것 같다고 생각되면 바로바로 슬랙에 이야기하고 추가적인 회의가 필요하다면 진행해 다 나은 방향으로 나아갈 수 있었던 스프린트였다.
윤승혁 — 간단한 조작법에 게임 목표도 비교적 단순하여 에셋의 중요성이 많이 부각되는 게임이라고 느꼈는데, 에셋을 만들어주신 분들이 정말 잘 만들어주셨다.
조휘현 — 튜토리얼 스테이지를 거의 완성해서 게임이 돌아가는 모습을 확인할 수 있게 되어서 좋았다. 직접 돌아가는걸 보니 예상하지 못한 버그나 문제점들도 더 발견할 수 있었고 고칠 수 있었다.
잘 안 된 부분
백수지 — 회의를 통해 사전에 에셋별 기능이나 플레이어, 카메라 움직임 방식 등에 대한 상세한 정의가 이루어지지 못했다. 이로 인해 작업 도중 작업 방향에 대해 서로 공유받지 못하는 상황이 생기거나 서로 이해한 내용이 상충돼 다시 작업을 해야 하는 상황들이 있었다. Sprint3에서 Stage를 확장하기 전에 에셋별 기능, 전체 플레이 시나리오 등에 대한 재확립이 이루어진다면 개발이 더 빠르게 이루어질 것이라고 생각한다.
오제헌 — 잘 되지 않은 것을 수정하려는 노력이 많았다는 것은 그만큼 잘 되지 않은 것이 많았다는 뜻이다. 대부분이 회의를 통한 사전 논의가 완전히 이루어지지 않았기 때문이었다. 어떤 일을 하자라고 정하기는 했지만 너무 추상적이었다. 계획 시점에서 task를 더 세세하게 나누고 detail에 대한 논의가 충분히 이루어졌다면 진행 중 막히거나 추가 회의가 더 적게 필요했을 것이다.
윤승혁 — 현재 스프린트까지 이동과 카메라 관련 스크립트가 완성이 되었으면 좋았을텐데, 완성하지 못하였다. 원래 계획과 많이 달라진 이유도 있었지만 뚜렷한 목표를 가지지 않고 대강의 틀만 잡아놓고 작업을 했기 때문이라고 생각한다. 다음 스프린트에서는 구상 단계를 탄탄하게 다지고 가면 좋을 것이라고 생각한다.
조휘현 — 처음에 스프린트 3의 작업이었던 ui 작업까지 할 수 있을 것 같아서 끌어왔었는데, 예상보다 작업량이 많아서 하지 못했고 에셋도 만들어야 하는 개수가 계속 늘어나서 다 완성하지 못한게 아쉬웠었다. 구체적으로 내용을 정하지 않고 추상적인 내용만 정했던 것이 문제였던 것 같다.
권민정
- Observer Pattern을 Generic하게 구현하기 위한 클래스를 디자인함
- 이번 스프린트에서 많이 사용되지 않아도 추후 재사용하기 편할 것
- Pair Programming의 적절한 활용
- 구에서 카메라가 움직이는 스크립트 디버깅 할 때 PP가 유용했음.
- 막히거나 디버깅해야 할 때, 로직을 정할 때 PP가 유용한듯. 반면 단순작업, 자료조사, 리팩토링은 혼자 하는게 편한듯
- git 사용에 익숙해지기
- conflict가 많이 났다. scene은 여러 명이 건드리지 않는 것으로
- player, camera 같은 모든 씬에 적용되는 스크립트 작업시에는 진짜 씬에서 작업하지 말고 test scene에서 작업하기? 진짜 씬을 보호하기 위해 그래도 좋을듯
- PR 메세지가 더 자세해야 할 듯. 작업내용을 모두에게 잘 설명해서 follow up cost 줄이기의 필요성 (더 친절한 주석, 배치에 대한 설명 등)
- 코드의 구조를 논의하여 정하고 개발을 시작해야 하나, 하면서 어차피 엎어지니 하고 정리하는게 나은가?
- 이번 스프린트에서 본격적인 개발을 하며 얻은 인사이트로 다음 스프린트부터는 구조를 미리 정하는게 가능할 수도 있겠다.
- 시간배분의 필요성
- conflict가 나는 작업의 경우엔 번갈아가며 작업하는 것이 좋을 것 같다 (어차피 모두가 하루종일 소개원실만 할 순 없으니)
장애물과의 상호작용을 플레이어인 우주인 script 내에서 모두 처리하는 것보다 각각의 장애물에 대한 script를 따로 작성하는 방식으로 구현하기로 하여 새로운 프로덕트 백로그로 추가하였다. 또한 게임 플레이 도중 마주치는 퍼즐을 모델링하고 이를 구현하는 script 역시 추가되었다. 스프린트2에서 튜토리얼 스테이지를 먼저 플레이 가능하게 만든 후 다음 스테이지들로 확장하겠다는 목표를 세우면서 게임 플레이 자체에 필수적인 UI의 프로덕트 백로그 우선순위도 상승하게 되었다.
현재 플레이어 이동 방식은 마우스 위치에 따라 카메라와 플레이어의 방향을 조정하고, WASD 키를 누르면 해당 방향으로 플레이어가 이동하는 구조이다. 기존에는 CameraManager가 마우스 입력을 처리하여 방향을 계산하고, 카메라의 방향과 playerTransform의 방향을 모두 변경했다. 하지만 이 방식은 클래스는 하나의 책임을 가져야 하는 Single Responsibility Principle을 위반한다. Camera만을 관리해야 하는 CameraManager가 플레이어의 회전을 변경하는 것은 SRP에 위배되는 상황이다. 이렇게 플레이어의 회전을 여러 객체가 변경하다 보면 객체의 회전이 어디에서 변경되는지 추적하기 어려운 문제가 발생할 수 있다.
따라서, CameraManager에서 PlayerManager의 UpdateRotation() 함수를 호출해서 회전해야 하는 방향을 전달하고 Player의 방향은 Player의 Update() 함수에서 변경하도록 수정했다.
public class CameraManager : MonoBehaviour
{
...
private void UpdateCameraPosition()
{
...
if (!Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift))
UpdatePlayerRotation();
}
private void UpdatePlayerRotation()
{
Vector3 directionToCamera = transform.localPosition - playerTransform.localPosition;
directionToCamera.y = 0;
Quaternion targetRotation = Quaternion.LookRotation(-directionToCamera);
// Update playerTransform in CameraManager Class
**playerTransform.localRotation = Quaternion.Slerp(playerTransform.localRotation, targetRotation, Time.deltaTime * 1000);**
}
}
public class CameraManager : MonoBehaviour
{
...
private void UpdateCameraPosition()
{
...
if (!Input.GetKey(KeyCode.LeftShift) && !Input.GetKey(KeyCode.RightShift))
{
Vector3 directionToCamera = transform.localPosition - playerTransform.localPosition;
directionToCamera.y = 0;
Quaternion targetRot = Quaternion.LookRotation(-directionToCamera);
**playerManager.UpdateRotation(targetRot);**
}
}
}
public class PlayerManager : MonoBehaviour
{
Quaternion targetRot;
void Update()
{
...
**transform.localRotation = Quaternion.Slerp(transform.localRotation, targetRotation, Time.deltaTime * 1000);**
}
public void UpdateRotation(Quaternion targetRot)
{
targetRotation = targetRot;
}
}
Gravitable Escape에서는 플레이어에게 해를 가하는 다양한 오브젝트가 등장한다. 예를 들어, 플레이어가 가시의 윗면과 충돌하거나, 폭탄이 터질 때 근처에 있거나, 중력을 전환하여 무거운 물체에 깔리면 플레이어의 생명이 줄어든다. 이처럼 가시, 폭탄, 무거운 물체는 모두 플레이어의 생명을 줄어들게 한다는 공통점을 가진다.
이 공통점을 기반으로, 이러한 오브젝트들을 효율적으로 관리하기 위해 HazardManager라는 abstract class를 정의하였다.
Liskov Substitution Principle, Interface Segregation Principle 을 고려하여 가시, 폭탄, 무거운 물체들의 최소한의 공통 요소인 damage, HarmPlayer()만을 HazardManager에서 가지도록 설계했다. Dependency Inversion Principle을 고려하여 IPlayerManager 인터페이스를 두었다. abstract class인 HazardManager에서 PlayerManager의 구체적인 구현의 ModifyLife 함수를 호출하여 플레이어의 생명을 줄어들게 하는 것은 Dependency Inversion Principle에 어긋난다고 생각하여 PlayerManager의 인터페이스 IPlayerManager를 만들어서 ModifyLife 함수를 정의했다.
Sprint2에서는 ThornManager까지만 구현하였지만, 추후 스프린트에서 BombManager, HeavyObjectManager를 구현할 때도 HazardManager를 사용할 예정이다.
public interface IPlayerManager
{
public void ModifyLife(int amount);
public int GetLife();
}
public abstract class HazardManager : MonoBehaviour
{
protected int damage;
protected abstract void HarmPlayer(IPlayerManager player)
{
player.ModifyLife(-damage);
}
}
public class ThornManager : HazardManager
{
private void OnCollisionEnter(Collision collision)
{
...
HarmPlayer(player);
...
}
}
게임에서는 하나의 이벤트에 다양한 물체가 영향을 받는 경우가 많다. Gravitable Escape에서는 중력이 바뀔 때 카메라와 플레이어가 모두 회전해야 하고, Game Over가 되면 모든 오브젝트가 비활성화되어야 한다. 이런 이벤트를 여러 객체에게 알려주는 방법으로 Observer Pattern이 유용할 것이라고 생각하여, 이를 각 주제마다 Observer, NotifyType(OnNotify의 인자 타입)에 대해 Generic하게 사용할 수 있도록 Subject 클래스를 설계하였다.(GameCore.cs)
스프린트 중간의 코드에서는 CameraManager, PlayerManager에 GravityObserver를 적용했지만, 움직임 방식을 바꾸면서 스프린트 종료시점인 현재에는 적용되지 않은 상태이다. 추후 스프린트에서 코드를 수정하면서 다시 적용할 예정이고, 다른 이벤트들 (Game Over, Player fainted 등) 에도 적용할 예정이다.
class Subject<Observer, NotifyType>
{
private List<Observer<NotifyType>> observers = new List<Observer<NotifyType>>();
public void AddObserver(Observer<NotifyType> observer)
{
if (!observers.Contains(observer))
{
observers.Add(observer);
}
}
public void RemoveObserver(Observer<NotifyType> observer)
{
if (observers.Contains(observer))
{
observers.Remove(observer);
}
}
public void NotifyObservers(NotifyType data)
{
foreach (Observer<NotifyType> observer in observers)
{
observer.OnNotify(data);
}
}
}
public interface Observer<NotifyType>
{
void OnNotify(NotifyType data);
}
public interface GravityObserver : Observer<Quaternion> { }
public interface GameOverObserver : Observer<bool> { }
public class GravityManager : MonoBehaviour
{
...
Subject<GravityObserver, Quaternion> gravityObs;
gravityObs = new Subject<GravityObserver, Quaternion>();
gravityObs.AddObserver(...);
gravityObs.NotifyObserver(transform.rotation);
...
}
public class PlayerManager : MonoBehaviour, GravityObserver
{
public void OnNotify(Quaternion gravityRot)
{
transform.rotation = gravityRot;
}
}