현재 재직중인 회사에서, 자율 회로 조립 시뮬레이션 컨텐츠를 추가 하기로 하였고, 1월 중순에 1차 마무리가 되었다. 정리를 하자하자 하고 나서 이제야 하게 되었는데, 바로바로 정리하는 습관을 기르도록 해야겠다.
작업 영상
Structure(구조)
대략적인 구조를 그리자면 위와 같다. 위에서 부터 전체적인 그림부터 그밑의 작은 부분들로 구분해보도록 하겠다.
- page(UI)
- 기본적으로 ui를 위한 frame구조가 있다. 그중 비즈니스 로직의 주된 처리를 담당하는 controller에서 주요 로직들을 담당하고 있다. Input Contoller, Assembly Controller들을 할당 하고, ui의 이벤트가 일어났을 때, 그에 걸맞는 시뮬레이션 모델들을 조작한다.
- page에서는 주로 ui의 변경에 이용되는데, led의 색 변경, 저항의 저항값 변경을 위한 설정등을 가지고 있다.
- 위에 서는 안나타나 있지만, page -> controller로 연결 되어있다고 생각하면 된다.
- controller(Assembly, Input)
- AssemblingController : component(모델)의 움직임, 선택, 위취 확정등의 컨트롤을 담당. contoller(frame) -> AssemblingController.
- 하지만, component의 움직임은 주로 Ui(MoveHandler)를 통해 input 값이 들어온다. 그렇기 때문에, move handler를 따로연결을 해줬다. -> ComponentCanvas Behaviour이 할당된 이유.
- inputController : 터치 이벤트가 들어왔을 때,
- 어떤 터치 이벤트인지(finger up, finger on, finger stay 등)
- 위의 이벤트가 어느 영역에서 일어난지 구분. physics.raycastall과 leantouch.raycastgui를 사용
- 구분이 되었으면, 그구분에 따라 로직을 실행해준다.
- 로직을 실행해준다.라는 부분이 어려웠는 데, 만약 컴포넌트에서 finger down일때, 일어나는 로직이라면 component를 가져오고 그컴포넌트 내부에 그에 상응하는 로직이 구현 되어 있어야 했다.
- 그렇다면 component를 어떻게 가져오고, 그 컴포넌트 내부를 어떻게 설계할까가 과제였다.
- 결국, select시나리오에서 getcomponent로, component(모델)클래스를 가져오고, 컴포넌트 부모클래스에 input동작에 따른 추상 메소드를 선언. gui일 경우에도 마찬가지.
- 정리 :
- 추가 노트 : Action?.Invoke()으로 메소드를 등록 해주기전에는, 클래스 내부에서 필요한 로직을 실행하기 위해 필요한 컴포넌트들을 일일히 할당해줬었다. 의존성이 높았다.
Overall
주요기능(Main Feature)
여러가지 기능을 다뤘지만, 그중 기억에 남고 공을 조금 들인 기능들에 대해서 정리하겠다.
- 브레드 보드위에 컴포넌트별로 알맞은 위치에 고정
- 브레드보드위에 옮기다가 가장 가깝고, 올바른 위치에 일정 반경안으로 들어오면 그 올바른 위치 값으로 이동.(자석 처럼 보여서, 자석기능이라 명칭함.)
- 컴포넌트를 3차원에서 z 값이 0일때, 그리고 컴포넌트의 회전축을 기준으로 worldspace에서 이동.
- 상위 컴포넌트와 하위 컴포넌트로 나눈후, 하위 컴포넌트가 상위 컴포넌트와 합쳐졌을 때, 분리 됐을 때의 기능
브레드 보드위에 컴포넌트별로 알맞은 위치에 고정
자석기능을 구현 하기 위해서는, 1) 해당 컴포넌트에 가장 가까운 브레드보드 홀을 구하고, 2) 일정 반경 안에 들어서면 그위치로 들어가게 해준다. 3) 그리고, 포인터의 위치가 반경을 넘어가는 순간 오브젝트는 포인터의 위치로 가게 해줘야 한다.
-> oncollision를 쓰게 되면 2번 까지는 잘 돌아가게 되지만, 포인터의 위치가 반경으로 넘어가는 순간에 오브젝트의 이동이 씹히거나, 행위가 매우 부자연스러운 경우가 많았다. (브레드보드 홀사이의 간격이 좁은 이유도 있다.)
해결방안 : 컴포넌트의 gizmo(+collider box, 사실 이게 메인으로 detection하는 친구)를 그리고 gizmo를 먼저 움직인 다음 컴포넌트를 기즈모에 따라서 움직여 준다. gizmo가 조건을 만족하면 모델은 그위치로 고정 되고, 다시 gizmo가 조건을 만족하지 않으면 모델은 gizmo를 따라서 움직인다.
private Vector3 DetectCollisions() {
var hitColliders = Physics.OverlapBox(
GetComponent<BoxCollider>().center * colliderSize + followGizmo,
GetComponent<BoxCollider>().size * colliderSize / 2,
Quaternion.identity,
layerMask);
int length = hitColliders.Length;
followGizmo.y = componentStartPos.y;
//superior 컴포넌트와 마주치는 순간, 보드 위에 띄워주기
for (var i = 0; i < length; i++) {
if (hitColliders[i].CompareTag("Player")) {
collidedComp = hitColliders[i].GetComponent<SuperiorComponent>();
followGizmo.y = componentStartPos.y + move_Yaxis_OnBoard;
}
}
//홀위에 컴포넌트 별 커버하는 홀의 갯수이상일 때, 가까운 홀과의 거리를 구해준다.
if (length >= component_size) {
if (collidedComp != null) {
//컴포넌트를 위치 확정 가능 상태로 바꿔준다.
IsConfirmable = true;
}
return GetClosest(hitColliders);
}
IsConfirmable = false;
return followGizmo;
}
컴포넌트의 회전축을 기준으로 worldspace에서 이동.
일반 적으로, 화살표를 오브젝트 위에 띄우고, 오브젝트의 기준으로 world space상의 좌표를 움직여주면, x축, y축등 원하는 방향으로 움직이게 할수 있다.
-> 하지만, 회전을 해주고 나서 다시 그 화살표로 오브젝트를 움직이면, 그화살표 또한 같이 움직이게 되고, 기존의 x,y,z축을 기준으로 움직이게 된다.
-> 추가로 내가 화면을 터치하는 주소값은 screen position인데, 내 터치에 따라서 이 오브젝트를 움직이기 위해선, world position으로 입력값을 치환해줘야 한다. 여기서 문제가 발생. 일반적으로, screen position의 world position으로의 치환은 camera에서 distance값을 사용한다. 무슨 의미냐면, Leantouch의 getWorldposition을 보자.
public Vector3 GetWorldPosition(float distance, Camera camera = null)
{
// Make sure the camera exists
camera = LeanTouch.GetCamera(camera);
if (camera != null)
{
var point = new Vector3(ScreenPosition.x, ScreenPosition.y, distance);
return camera.ScreenToWorldPoint(point);
}
return default(Vector3);
}
스크린 포지션의 x,y값에 distance값을 넣은 vector를 사용한다. 즉, 스크린 x,y에 내가 지정한 범위 만큼만 치환이 이뤄진다.
나쁘지 않지만, 내가 원하는 동작을 위한 세팅은 아니다.
해결방안 : 컴포넌트가 움직이게 될 환경을 만들어준다. 컴포넌트는 (0,0,0)에서 생성되고, y가 0으로 고정된상태에서 움직이게 될것이니, 1) (0, -1, 0)의 좌표를 갖는 plane을 만들어주고, 2)카메라에서 스크린으로의 레이를 만들어준다. 3) 아까 만들어준 plane에서 ray를 cast해주고 distance를 받아온다. 4) 다시 그 ray를 distance값을 기준으로 worldpoint로 치환한다.
protected Plane plane = new Plane(Vector3.down, 0);
public Vector3 Convert(Vector3 data) {
//스크린 포지션을 ray로 치환
Ray ray = Camera.main.ScreenPointToRay(data);
Vector3 worldPos = default;
//그 ray를 plane을 다시 기준으로 하여 distance값을 받고
if (plane.Raycast(ray, out float distance)) {
//ray에서 위의 새로운 distance를 기준으로 world 좌표 생성.
worldPos = ray.GetPoint(distance);
}
return worldPos;
}
상위 컴포넌트와 하위 컴포넌트로 나눈후, 하위 컴포넌트가 상위 컴포넌트와 합쳐졌을 때, 분리 됐을 때의 기능
사실 이부분은 크게 별건 없었다. 상위 컴포넌트와 하위 컴포넌트의 연결 타이밍과 분리 타이밍이 제일 관건 이었다.
시나리오 상에서 타이밍은 간단했지만, 이미 코드상에서 이미 짜여진 코드안에 기능을 넣어야했는데, 잘 구동을 하지만, 좋은 코딩이 맞는 지 의구심이 들었다. 전체적으로 코드리뷰를 해보았으면 하는 생각이 여기서 조금 들었다.
public void SetInferiorToSuperior() {
tr_Parent = collidedComp.transform.parent;
transform.SetParent(tr_Parent, true);
}
private void UnsetInferiorToSuperior() {
collidedComp = null;
transform.parent = null;
}
private void OnClickCheck() {
var inferior = assembler.selectedComponent.GetComponent<InferiorComponent>();
//체크 했을때 리스트에 넣고 (inferiror 제외) -> 전체 삭제할때 전체 삭제 해줄 리스트
if (inferior != null) {
//inferior는 superior로 설정
inferior.SetInferiorToSuperior();
}
}
private Vector3 DetectCollisions() {
var hitColliders = Physics.OverlapBox(
GetComponent<BoxCollider>().center * colliderSize + followGizmo,
GetComponent<BoxCollider>().size * colliderSize / 2,
Quaternion.identity,
layerMask);
int length = hitColliders.Length;
for (var i = 0; i < length; i++) {
if (hitColliders[i].CompareTag("Player")) {
collidedComp = hitColliders[i].GetComponent<SuperiorComponent>();
}
}
UnsetInferiorToSuperior();
return followGizmo;
}
보다시피, 필요하다고 느껴지는 부분 OnClickCheck, DetectCollision에 들어간 모습이 보인다.
Detectcollision에서는 콜리전의 체크만 해야하는 데, Unsetinferior~메소드가 들어가 있다.
해결방안 : 디테일한 기능을 개발하고, 메소드를 짤때, 단일 기능 단위로 자는 연습을 해야겠다. 위의 문제는 추후에 수정해야겠다.
이 프로젝트를 계기로 전체적인 구조나 개발의 방향성을 고려하는 능력이 길러진 것 같다.
시야가 넓어진 것도 있고, 개발이 재밌다고 느껴지는 프로젝트 였다. 한가지 아쉬운 점은 확장성에 대한 고려나 디자인 패턴 및 객체지향의 익숙함이 부족 했던 것 같다.
패턴과 oop 4원칙의 중요성을 다시 깨달음..
'Game Dev > Unity' 카테고리의 다른 글
Unitask의 정체 - (feat. task, 비동기, Unity는 싱글스레드?) (0) | 2023.04.09 |
---|---|
Unity로 배우는 game dev architecture : Part1_SOLID (0) | 2023.03.18 |
wifi로 unity 빌드 하는 법-초간단 (1) | 2023.01.08 |
Scriptable Object 개요 (1) | 2022.10.03 |
스택 계산기를 활용한 TagParser (0) | 2022.07.24 |