유니티 엔진은 처음의 개발 목표가 비프로그래머를 위한 게임 개발 도구를 만드는 것이었다. 그렇기 때문에 게임 기획자나 디자이너가 에디터를 이용하여 프로그래머 도움없이 게임 수정이 가능하다. 이런 유니티 엔진의 특성을 활용하여 기획자가 스스로 게임을 계속 수정해가면서 최적의 상태를 찾을 수 있게 된다. 이런 툴기반 게임 제작 환경을 만들기 위해서는 코드 중심이 아닌 에디터에서 편집 가능한 컴포넌트 기반으로 프로그램을 제작해야 한다. 이때 중요한 점은 컴포넌트간에 직접적인 의존성을 가지게 하는 프로시저 기 통신을 지양하는 것이다.
컴포넌트 기반 프로그래밍
컴포넌트란 범용적인 개발을 위해 기능별로 모듈화한 부품 단위를 말한다. 컴포넌트 기반 프로그래밍 기법은 특정 객체가 해야할 기능을 여러개의 컴포넌트를 조합하여 구현 하는 방식이므로 범용적이고 유연성이 좋다. 게임안에서 등장하는 몬스터를 출력한다고 해보자. 일반적인 계층 구조 방식으로 이를 구현할 경우 여러가지 몬스터 종류가 늘어날 수록 상위 객체에 구현되어지는 코드량이 커지고 이로 인해 사용하지도 않는 기능을 상속받게 된다. 이렇게 객체 종류가 늘어나면 날수록 코드의 응집도가 떨어지게 된다. 반대로 하위 객체에 기능을 넣다보면 중복 코드가 발생하게 된다. 이런 상황에서 컴포넌트 기반 설계의 장점이 발휘된다. 각 객체별로 필요한 컴포넌트를 조합해서 사용해주기만 하면 객체 종류가 늘어날 필요없이 여러 상황에 대처할 수 있다. 기능별로 코드가 분리되어 있으므로 응집도가 높으며 특정 구현 코드가 집중될 염려도 사라진다.
프로그래머들은 기존의 코드 중심 객체 개발 패러다임에서 벗어나야 한다. 컴포넌트를 기존에 익숙했던 절차적 프로그래밍 기법을 이용하여 만들어 내는 것은 잘못된 사용 방식이다. 이렇게 사용할 경우 유니티 엔진 도입의 장점이 적어진다. 메시지 기반 통신을 해야 비 프로그래머도 편집 가능한 컴포넌트들을 만들어 낼 수 있다. 따라서 유니티 에디터 확장 기능을 최대한 활용하여 편집 가능한 방식으로 제작한다.
컴포넌트 기반으로 개발하기 위해서는 프로그래머는 코드가 아닌 데이터 기반으로 제작을 해야 한다. 서버 접속 주소/ 각종 옵션은 반드시 코드가 아닌 외부 데이터 파일에 기술하거나 서버로 부터 응답을 받아 처리해야 할 것이다. 그렇게 하려면 데이터셋들에 대해서 필히 버전 관리를 해야 한다. 이런 버전관리 대상에는 실시간 다운로드 게임 데이터, 데이터베이스 스키마 데이터, 그래픽 리소스 데이터, 게임 기획 문서 등 프로젝트의 모든 데이터를 대상으로 해야 한다.
컴포넌트와 데이터 기반으로 제작 프로세스를 가져가면 마지막으로 이렇게 구성된 시스템들을 에디터로 통해 수정할 수 있는 기반이 갖춰지게 된다. 게임 기획자가 유니티 에디터를 통해 파라미터와 게임 데이터를 교체할 수 있도록 제공해야 기획자들이 반복 테스트를 통해 최적의 게임성을 찾을 수 있게 된다.
이벤트 기반 프로그래밍
이렇게 컴포넌트로 객체를 분리하면 컴포넌트간의 참조가 어려워진다. 컴포넌트간의 복잡한 의존성을 끓어내기 위해서는 이벤트 기반 프로그래밍 기법이 중요하다.
이벤트 통신을 이용하면 컴포넌트간 의존성을 줄여줄 수 있다 . 하지만 컴포넌트는 밀접한 관계가 있는 것끼리 부모 자식 관계로 구성을 하여 부품 처럼 사용되는 컴포넌트는 GetComponent등의 함수로 직접 찾거나 중객자 객체를 통하거나 에디터에서 설정하는 식으로 참조를 해도 된다. 브로드캐스트는 느리므로 왠만하면 타입별 브로드캐스트를 사용하거나 브로드캐스트용 중개자들 두는 방식으로 사용한다.
즉 밀접한 관계를 가지는 경우에 생기는 의존성을 큰 문제가 아니다 그외
AMVVMC / AMVW(Whatever)C / AMVPC 아키텍쳐
프로그래밍의 역사를 보면 프로젝트가 커지면 로직이 복잡해짐에 따라 의존성이 강해지면서 유지보수가 어려워 진다. 이를 해결하기 위해 프로그램을 모델, 뷰, 컨트롤러로 분리하는 MVC 패턴이 나타나게 되었다. 이후 MVC를 시작으로 발전한 여러 변종이 있지만 결국 모델과 뷰 사이의 관계를 어떻게 처리하느냐에 따라 패턴을 구별할 수 있다. 모델과 뷰를 관계를 여러가지 방식으로 다루는 수많은 변종들이 존재하고 명칭도 섞어 쓰는 경향이 있기 때문에 심지어 MVW(Model-View-Whatever) 패턴이라고 부르기도 한다. 각자의 개발 상황이나 혼경에 따라 적절하게 이용하는게 좋을 것같다.
이중 요즘 많이 쓰이는 MVVM은 MVC에서 시작되었다. MVVM에 정석 구현은 MS의 WTF라고 볼수 있지만 완벽하게 뷰와 뷰모델이 분리된 형태가 아니라 옵저버 패턴이나 이벤트 처리 메카니즘등을 통해 MVVM 최대한 가깝게 구현한 시스템들도 존재한다. MVVM을 제대로 구현하면 각 객체들간의 직접적인 의존성이 모두 사라진다. 하지만 MVVM의 단점은 뷰모델 설계가 어려울 수 있다는 것이다.
MVVM의 사고 방식은 느슨하게 결합 된 방식으로 계층을 분리하여 서로 간섭하지 않고 각 계층을 수정하고 테스트 할 수 있도록하는 것이다. MVC에서 파생되었으므로 모델과 뷰간의 의존성 줄여주고, 여기에 뷰와 뷰모델(컨트롤러, 프리젠터)간의 의존성도 줄여서 각 구성요소가 독립적으로 작성되고 테스트될 수 있도록 설계된 아키텍쳐이다. MVVM을 완벽히 구현하려면 뷰와 뷰모델 사이의 종속성이 모두 제거되야만 한다.
MVP에서는 컨트롤러를 프리젠터 객체에 상호작용을 모아서 뷰와 모델을 분리하는 개념이라면 MVVM은 컨트롤러를 뷰모델이라는 뷰에 특화된 상호작용 처리 객체를 통해 뷰와 모델을 분리하는 개념이다. 거기에 더해 뷰와 뷰모델의 의존성도 줄여서 각가 독립적으로 동작할 수 있게 하는 아키텍쳐이다.
뷰모델은 간단한 구현에서는 프리젠터와 비슷해보일 수 있다. 결국 태생이 컨트롤러에서 시작되었기 때문이다. 하지만 MVP에서는 컨트롤러를 프리젠터 객체에 상호작용을 모아서 뷰와 모델을 분리하는 개념이고 MVVM은 컨트롤러를 뷰모델이라는 뷰에 특화된 상호작용 처리 객체를 통해 뷰와 모델을 분리하는 개념이다. 즉 뷰모델을 제작할때 이름 그대로 뷰의 특화된 모델 객체라고 생각하고 디자인해야 한다.
MVVM은 모든 입력이 뷰에서 들어오고 뷰모델은 상호작용(프리젠테이션)을 처리하고 뷰에 데이터를 전달한다. 뷰모델이 변경되면 뷰모델을 이용하는 뷰들은 자동으로 업데이트 되게 되는데 이 자동으로 업데이트 시키는 바인딩 구현 메카니즘에 따라 뷰와 뷰모델의 의존성이 차이가 난다.
뷰와 뷰모델의 관계를 데이터 바인딩 방식으로 설계하면 모든 객체들간의 의존성이 사라지므로 깔끔해지지만 유니티의 경우 내장된 UI 바인딩 메카니즘을 제공하지 않고 이를 만드는 것은 복잡하고 오버헤드가 발생하기 쉽다.
따라서 MVP 패턴에서 뷰와 프리젠터 객체간의 의존성을 줄이기 위해 사용되는 Reactive Presenter 패턴을 참고한다. MVP 패턴을 사용하는 프리젠터는 뷰의 구성요소를 알고 있으며 업데이트 할 수 있다. 실제 바인딩을 하지 않지만, 뷰를 구독(Observable)하여 바인딩 하는 것과 유사하게 동작하게 할 수 있다. (복잡하지 않고, 오버 헤드도 적게 사용 가능) (https://tech.lonpeach.com/2020/11/09/Thinking-about-MVRP/ )이런 옵저버 형태의 패턴을 도입하면 뷰는 프리젠터에 대한 의존성이 사라진다.
우리가 구현할 MVVM 구조에서는 뷰와 뷰모델간의 의존성은 Reactive Presenter 패턴처럼 옵저버 객체로 처리하거나 이벤트 처리기를 통해 줄일 수 있다( 이벤트 처리기도 옵저버 형태의 구독 방식이라고 생각해도 무방한다). 유니티에서 이런 Reactive 형태 바인딩 처리를 쉽게하기 위한 라이브러리로는 UniRx가 있다. MVVM 패턴에 Reactive Presenter 패턴을 적용하려면 뷰모델을 프리젠터 객체라고 생각하면 된다.
이제 MVVM의 그림이 나왔다. 일단 다음과 같이컴포넌트가 어떤 역할을 하는지부터 정의한 후 개발을 한다.
이때 각 객체의 네임스페이스는 각 Application,Model,View,ViewModel이라는 그룹하위에서 관리하는 식으로 생각해야 한다. 각 객체는 하나의 큰 레이어를 의미하기 때문에 독립적으로 처리한다고 봐야 한다. 다른 프로젝트에서 MVVM 객체들을 어떻게 그룹핑하는지는 좀더 조사가 필요하다 (https://github.com/MEyes/uMVVM/tree/master/Assets/Sources )
Application
단일 엔트르 포인트 컴포넌트의 역할은 Application이 하게 된다.
Model
Model 은 응용프로그램의 비즈니스 로직과 데이터를 캡슐화
View
View는 UI 와 UI 로직을 캡슐화
ViewModel
ViewModel은 프리젠테이션 로직과 상태를 캡슐화. ViewModel은 View를 나타내는 모델이면서 View의 상호작용을 처리하는 프리젠테이션 로직이기도 하다.
Component
활용예
MVVM은 AAA 소프트웨어 하우스에서 게임을 만드는 데 성공적으로 사용되었습니 다. Shipbreakers ( Homeworld : Deserts of Kharak ) 의 제작자는 View가 MonoBehavor라고 생각하고, VM과 M은 Unity 외부에서 독립적으로 작성된 POCO 클래스로 개발 속도를 높이도록 코드를 작성했다. 이러게 작성을 하게되면 MonoBehaviour보다 가벼운 객체로 VM, M을 작성할 수 있어서 가볍기는 하지만 유니티 엔진 API를 사용하지 못하는 단점이 있으므로 대규모 게임에서 고려해볼만한 방법이라 할 수 있다.
Conclusion
MVVM 패턴을 사용하면, 응용프로그램의 UI 와 기저에 깔린 프리젠테이션 로직 및 비즈니스 로직이 세 개의 분리된 객체로 나뉜다 뷰는 UI 와 UI 로직을 캡슐화한다. 뷰모델은 프리젠테이션 로직과 상태를 캡슐화한다. 모델은 응용프로그램의 비즈니스 로직과 데이터를 캡슐화한다.
MVVM은 MVP와 비슷한 변종이며 데이터 바인딩을 이용하여 객체간의 의존성을 더 낮출 수 있다. 뷰의 바인딩 메카니즘중 가장 의존성이 적은 것은 코드 형태로 바인딩을 하는것이 아니라 데이터 템플릿 정의를 참조만 하도록해서 로직이 아닌 데이터에만 의존하게 하는 것이다. 어쨋든 C#이나 자체 데이터바인딩을 연구해서 최대한 프리젠테이션 코드를 뷰에서 완전히 제거하고 뷰는 데이터 템플릿이나 옵저버 패턴중 옵저퍼 패턴의 하나인 Reactive Presenter 패턴을 이용한 수준으로 구현하기로 한다.
이렇게 보면 Reactive Presenter를 도입한 MVP 패턴에 더 가까워보일 수 있지만 우리는 프리젠터 객체가 아니라 뷰의 상호 작용을 처리하는 뷰모델 객체 관점에서 제작할 것이고 바인딩 메카니즘을 옵저버 방식의 코드로 하는 구현도 많이 볼 수 있다. 왜 MVVM 구조가 필요한가? 여러가지 장점들은 많지만 일단 뷰와 로직을 분리해야 TDD 같은 기법을 통해 테스트 자동화를 이를 수 있다. 유니티가 되었든 C# 환경이 되었든 MVVM 에서 가장 중요한 것은 뷰의 바인딩 메카니즘이 중요한 것이 아니라 코드가 되었든 템플릿 형태가 되었든 뷰모델을 따로 테스트 가능하도록 만드는 것이 중요하다.
다음은 유니티 에디터툴 기반으로 바인딩하는 방법으로 구현한 MVVM 구현물이다. 유니티 환경에서는 에디터를 이용하여 바인딩 처리하는 것이 알맞아 보인다.
https://github.com/Real-Serious-Games/Unity-Weld-Examples
다음은 MVC 설계를 유니티에 적용한 방식이다. 우리가 사용할 방식은 MVVM이지만 유니티에서 MVC 종류의 패턴을 구현하는데 좋은 지침이 되기 때문에 좋은 시작점이 될 것이다.
https://www.toptal.com/unity-unity3d/unity-with-mvc-how-to-level-up-your-game-development
- app [Application] - game [Game] - player [KeyboardInput, Renderer] - enemies - spider [SpiderAI, Renderer] - ogre [OgreAI, Renderer] - ui [UI] - hud [HUD, MouseInput, Renderer] - pause-menu [PauseMenu, MouseInput, Renderer] - victory-modal [VictoryModal, MouseInput, Renderer] - defeat-modal [DefeatModal, MouseInput, Renderer]
- Application – Single entry point to your application and container of all critical instances and application-related data.
- MVC – You should know this by now. 🙂
- Component – Small, well-contained script that can be reused.
- Data: Go to
application > model > ...
- Logic/Workflow: Go to
application > controller > ...
- Rendering/Interface/Detection: Go to
application > view > ...
// BounceApplication.cs // Base class for all elements in this application. public class BounceElement : MonoBehaviour { // Gives access to the application and all instances. public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }} } // 10 Bounces Entry Point. public class BounceApplication : MonoBehaviour { // Reference to the root instances of the MVC. public BounceModel model; public BounceView view; public BounceController controller; // Init things here void Start() { } } // BounceModel.cs // Contains all data related to the app. public class BounceModel : BounceElement { // Data public int bounces; public int winCondition; } // BounceView .cs // Contains all views related to the app. public class BounceView : BounceElement { // Reference to the ball public BallView ball; } // BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.controller.OnBallGroundHit(); } //이벤트 처리기로 처리 가능 } // BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnBallGroundHit() { app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball OnGameComplete(); } } // Handles the win condition public void OnGameComplete() { Debug.Log(“Victory!!”); } }
모델
- 응용 프로그램의 핵심 데이터 및 상태 (예 : player
health
또는 gun)를 보관ammo
합니다. - 형식 간 직렬화, 역 직렬화 및 / 또는 변환.
- 데이터를로드 / 저장합니다 (로컬 또는 웹에서).
- 컨트롤러에게 작업 진행 상황을 알립니다.
- 게임의 유한 상태 머신에 대한 게임 상태를 저장합니다 .
- 뷰에 액세스하지 마십시오.
뷰
- 사용자에게 최신 게임 상태를 나타 내기 위해 모델에서 데이터를 가져올 수 있습니다. 예를 들어, View 메서드
player.Run()
는 내부적으로model.speed
플레이어 능력을 나타내는 데 사용할 수 있습니다 . - 모델을 변경해서는 안됩니다.
- 해당 클래스의 기능을 엄격하게 구현합니다. 예를 들면 :
- A
PlayerView
는 입력 감지를 구현하거나 게임 상태를 수정해서는 안됩니다. - 뷰는 인터페이스가 있고 중요한 이벤트를 알리는 블랙 박스 역할을해야합니다.
- 핵심 데이터 (속도, 건강, 생명 등)를 저장하지 않습니다.
- A
컨트롤러(모델뷰)
- 핵심 데이터를 저장하지 마십시오.
- 때때로 원하지 않는보기에서 알림을 필터링 할 수 있습니다.
- 모델의 데이터를 업데이트하고 사용합니다.
- Unity의 씬 워크 플로를 관리합니다.
메타포 설계
메타포라는 것은 다른 대상을 통해서 대상을 이해하거나 경험하는 행위이다. 즉 우리가 이미 알고 있는 대상을 투영하여 알기 쉽게 설명할 수 있도록 하는 방법이다. 우리가 제작할 소프트웨어의 메타포는 리눅스나 운영 체제 시스템이다. GameObject는 리눅스나 원도우 파일 시스템 처럼 생각하고 관리한다. 리눅스는 일반적인 파일 뿐만 아니라 프로세스가 동작할때 /proc/daemon1와 같이 파일 시스템에서 프로세스나 운영체제의 여러 기능에 접근할 수 있다. Resources 폴더나 Assets에서 객체들은 운여체제의 디스크 파일 시스템이라 생각하고 생성된 GameObject 리스트는 특수 용도의 파일 시스템이라고 생각하고 관리한다. (https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC_%EC%8B%9C%EC%8A%A4%ED%85%9C )
In UNIX, everything is a file.
파일시스템이 있다면 프로그램이 있어야 한다. 컴포넌트 객체를 구현해서 운영체제에서 존재하는 로깅 서비스, 리소스 관리자, 네트워크 서비스등을 만든다. 운영체제 처럼 객체의 명령을 내리기 위한 콘솔 어플레키에션도 필요하다.
운영체제를 메타포로 보고 설계를 하게 되면 각종 컴포넌트의 이름을 짓기도 편해진다. 비슷한 역할을 하는 프로그램이나 서비스가 리눅스 운영체제에 있는지 찾아보고 이름을 흉내내어 지으면 되기 때문이다.