방학기간 동안 후디니 Joy of Vex를 마치고 지금 현재 학교에서 정신없이 여러가지들을 배우고 있다. 그 중 한 선택과목으로 유니티를 배우는데 컴퓨터 공학 전공자가 아닌 입장으로써 프로그래밍에 막연한 부담감과 어려움을 느꼈다. 기존에 배웠던 Java와의 문법 차이와 2D나 3D상에서 그래픽으로 나타내는 것에 까다로움을 느끼고 기존에 후디니 공부에 아주 유용하게 도움이 되었던 공부일지를 늦게나마 한 번 써보자 한다. 만약 이 글을 읽는 누군가가 틀린 정보를 찾거나 애매모호한 부분이 있다면 댓글로 피드백을 달아주면 환영하겠다.
0. Game Development
Game Development는 각기 여러 과정들을 통해 이루어지며 이를 담당하는 프로그래밍 언어들이 다르게 고루 분포되어 있다.
Gameplay | User Interface | Artificial Integlligence | Tools | Networking | Engine | Audio Processing | Rendering | Physics Simulation |
이 때 위의 표에서 좌측으로부터 세번째 단계 까지는 가상 머신이 담당할 수 있도록 C#이나 Python같은 프로그래밍 언어로 쓰여진다.
그리고 좌측으로부터 네번째 단계부터 마지막 까지는 특정 하드웨어가 담당할 수 있도록 C++같은 프로그래밍 언어로 쓰여지는 것을 알 수 있다.
1. C# Programming
Object-Oriented (using C#) | Component-based | Event-driven |
C#은 자바와 비슷한 Object-Oriented Programming Language (객체 지향 프로그래밍 언어)로써 .NET 프레임워크 기반으로 마이크로소프트에서 개발된 언어이다. 주로 유니티라는 게임 게발 도구의 프로그래밍 언어로써 쓰이고 있어 많이 게임 개발자들 사이에서 애용된다는 특징이 있다.
- .NET Libary
.NET API는 I/O, Math, strings, manipulation, collections networking과 같은 기능들을 위한 Methods들과 Classes들을 제공하는 라이브러리이다. 다만 유니티를 사용함에 있어 Unity API가 뭐든것을 다 지원해주기 때문에 많이 알 필요는 없다.
- Method (함수)
--> Player의 Heal 생명력 값을 다른 Method를 통해 값에 변화를 주는 방식 (이때 파라미터를 따로 불러와 Method 안 값에 적용시키는 방식으로 사용함)
// 아래 Heal()Method에서 얻은 return값을 Update() Method로 불러와 health값에 저장하는 방식
// 여기에 해당하는 부분을 Field라고 하며 heal이라는 variable이 global variable로 선언된것을 알 수 있다
float heal = 2f;
void Update ()
{
health = Heal (health); // health값은 아래 Method방식에 따라 제곱되어 4라는 값을 얻게된다.
}
int Heal (int currentHealth) // 함수를 설정할 함수 이름과 파라미터 값. (int)값을 반환하여라.
{
// currentHealth값을 제곱하여 currentHealth값에 저장
currentHealth = currentHealth * currentHealth;
Debug.Log ("Recovered" + currentHealth);
return currentHealth; // 함수가 값을 반환할때 사용
}
--> 만약 그냥 Method를 사용하기만 해도 Heal을 충전받는 그런 방식을 원한다고 할때 아래와 같은 방식으로 만들어 줄 수 있다.
// 아래 Heal()Method에서 얻은 return값을 Update() Method로 불러와 health값에 저장하는 방식
float heal = 2f; // Global Variable
void Update ()
{
int localVariable; // Local Variable (다른 Method)에서 사용할 수 없다
health = Heal (health); // health값은 아래 Method방식에 따라 제곱이 된다.
}
// 이때 아래 Method처럼 void로 Method를 선언을 해주면 굳이 Method안에서 쓰게될 다른 파라미터 값을 선언해주지 않고도 곧바로 다른 Method안에서 똑같은 Variable이름으로도 적용시켜줄 수 있다
void Heal () // 함수를 설정할 함수 이름과 파라미터 값. void임으로 변환값이 없다.
{
health = health * health;
Debug.Log ("Recovered" + health);
}
- Class (클라스)
Class는 하나의 사물 (GameObject)와 대응하는 로직이다. 즉 유니티 내에서 C# Script를 하나를 생성해주어 GameObject에 적용을 해줌으로써 Class를 생성시켜줄 수 있다.
public class Actor // Created the class called "Actor"
{
int id = 23423; // declaration of the int variable id
string Talk () // string Method
{
return "HELLO";
}
string HasWeapon() // string Method
{
return weapon;
}
void LevelUp() // void Method
{
level = level + 1; // level increases by 1
}
}
이때 다른 Class "Main"에서 Class "Actor"안에서 선언되었던 integer variable id를 가져다가 사용하고 싶다. 이때 어떻게 해야하나?
아랫 코드와 같이 Instatiation이라는 과정을 통해 다른 Class 자체를 하나의 Class 안에서 variable처럼 만들어 줄수 있으며 이때 생성된 Variable을 Instance (인스턴스)라고 부른다.
eg) Actor (다른 Class) actor (정보를 집어넣을 변수:인스턴스) = new Actor(); // actor는 Actor 클라스의 인스턴스
만약 new Actor()라는 값이 메모리에 할당되지 않을때 즉 eg) Actor actor; 으로써 선언만된다면 actor이라는 variable은 Object (객체) 라고 부르게 된다.
public class Main: MonoBehaviour
{
void Start()
{
// 이런식으로 쓰면 Class하나를 variable의 타입으로 바꿔서 사용할 수 있다.
// 이를 Instantiation (인스턴스화)라고 한다
Actor actor = new Actor();
// 이렇게 Instantiation된 variable actor에 .을 찍고 불러올 해당 class안 variable을 불러오게 할 수 있다.
Debug.Log (actor.id);
}
}
** 그런데 따라하다보면 이상함을 느낄것이다. actor.id라고 instance안 variable을 불러오게 되면 오류가 뜨는 것을 알 수 있다. 이는 Actor Class안에서 id variable값에 접근자가 없기 때문이다. 그러므로 선언전에 private인지 public인지 직접 써 넣어 주어야 다른 Class에서 variable을 불러올것인지 말지 설정해 줄수 있다.
- 수정된 코드
public class Actor // Created the class called "Actor"
{
public int id = 23423; // declaration of the int variable id. Accessor written.
string Talk () // string Method
{
return "HELLO";
}
string HasWeapon() // string Method
{
return weapon;
}
void LevelUp() // void Method
{
level = level + 1; // level increases by 1
}
}
- Child Class (자식 클라스)
그렇다면 궁금할 것이다. 도대체 지금까지 코드위에 써왔던 MonoBehaviour은 무엇을 의미하는 것이고 저 위치에 들어가는 것은 무엇일까?
public class Player : Actor // Player class는 Actor Class의 있는 정보를 모두 물려받는다
{
public int playerVariable = 123;
public string PlayerMethod ()
{
return "Player moves";
}
}
Class선언후 : 콜론 후에 들어가는 것도 Class임으로써 이는 Player이라는 Class가 Actor이라는 Class의 대한 정보를 모두 가져와 자신에게 포함시키겠다는 의미가 된다. (물려받음). 이때 Player Class가 Actor이라는 Class를 부모로 삼는다라고 말할 수 있다. 이를 Inheritance (상속)이라 하며 OOP에서의 아주 중요한 핵심적인 역할을 맏게 된다.
이러한 방식을 사용하여 이렇게 코드를 작성해 줄 수 있다. Child Class는 즉 부모 Class에 있는 variable과 method를 사용할 수 있으면서도 자기자신의 새로운 variable과 method를 추가로 사용하게 있게 한다.
public class Main: MonoBehaviour
{
void Start()
{
// 이런식으로 쓰면 Class하나를 variable의 타입으로 바꿔서 사용할 수 있다.
// 이를 Instantiation (인스턴스화)라고 한다
//Actor actor = new Actor();
// Player Class는 Actor Class를 부모로 삼고 있음므로 Actor Class안에 있는 정보를 Player Class에 있는 것 처럼 대신해서 불러올 수 있다.
Player player = new Player();
Debug.Log (player.id);
// 또한 Player Class안에 있는 독자적인 정보들을 불러오는것 또한 가능하여 이렇게 작성할 수 있다
// Player Class안에 있는 변수 playerVariable 정보를 불러와 콘솔에 호출
Debug.Log (player.playerVariable);
// Player Class안에 있는 메쏘드 playerMethod()의 return값을 불러와 콘솔에 호출
Debug.Log (player.playerMethod());
}
}

- MonoBehaviour
유니티에서는 기본적으로 내장된 Method들을 제공하고 있는데 이는 MonoBehaviour이라는 Class를 부모 Class로써 가져옴으로써 유저들에게 이용하게끔 만든다.
MonoBehaviour: The most common kind of computer component and the base class for components that implement most game mechanics. (거의 모든 게임 역학들을 실행 시킬 수 있는 Computer component이다).
MonoBehaviour이라는 Class를 기반으로 C#코드를 짤때 스크립트에는 맨 윗줄에 이 값들을 자동으로 생성시킨다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
- Start(): Start Method
Start() Method는 유니티에서 게임을 실행할때 Update() Method가 시작 직전에 최초로 실행되는 Method이다. Start()는 애초에 딱 한번만 실행되며 그다음 다른 Method가 실행되게끔 한다.
public class EventCycle : MonoBehaviour
{
void Start()
{
// Start()안에 string값을 콘솔창에 띄어 맨 첫번째 프레임에 한번만 콘솔에 나타나게끔 한다.
Debug.Log ("Game has started");
}
}
- FixedUpdate(): Fixed Update Method
이 Method는 유니티 엔진이 물리 연산을 하기 전에 실행되는 업데이트 Method이다. Update()와는 다르게 1초마다 실행되는 횟수가 정해져 있다는 특징을 가지고 있다.
FixedUpdate()는 1초에 50번 돈다고 가정했을때 고정된 실행주기로 Update() Method가 실행되어 CPU를 많이 소모한다는 특징이 있다.
public class EventCycle : MonoBehaviour
{
public FixedUpdate()
{
Debug.Log ("Method executed 50 times per second");
}
}
- Update(): Update Method
Update()는 물리 연산에 관련된 것을 제외한 나머지 주기적으로 변하는 로직을 넣을때 사용하는 Method이다. 다만 이 Method는 FixedUpdate()와는 달리 컴퓨터 사양에 따라서 1초마다 실행되는 횟수가 변하는 것을 알 수 있다.
public class EventCycle : MonoBehaviour
{
public Update()
{
Debug.Log ("Method executed random times per second");
}
}
- LateUpdate(): Late Update Method
LateUpdate()는 모든 Update()영역의 실행이 끝난뒤 마지막으로 호출되는 Method이다. 보통 캐릭터를 따라가는 카메라나 후처리의 대한 정보를 담는 method라고 보면 된다.
public class EventCycle : MonoBehaviour
{
public LateUpdate()
{
Debug.Log ("Method executed after every Update() frame");
}
}
- OnDestroy(): On Destroy Method
OnDestroy()는 프레임 영역이 끝난뒤 GameObject가 삭제가 되기 직전에 무언가 남기고 삭제된다는 그러한 의미로 쓰여지는 Method이다. 플레이어가 죽기전 콘솔에 죽었다는 메세지를 띄우거나 코인이 플레이어에게 먹혀서 사라졌다는 메세지를 띄울때 사용할 수 있을 것 같다.
public class EventCycle : MonoBehaviour
{
public OnDestroy()
{
Debug.Log ("The GameObject has been destroyed");
}
}
- OnEnable(): On Enable Method
OnEnable()은 GameObject가 활성화 되었을때 실행되는 Method이다. 이는 최초로 실행될때가만이 아닌 활성화 될때 실행된다는 특징이 있다. OnEnable()이 활성화 되면 그 다음 Method들이 차례대로 실행되기 시작한다.
public class EventCycle : MonoBehaviour
{
public OnEnable()
{
Debug.Log ("The event cycle has been activated");
}
}
- OnDisable(): On Disable Method
OnEnable()은 GameObject가 비활성화 되었을때 실행되는 Method이다.
public class EventCycle : MonoBehaviour
{
public OnDisable()
{
Debug.Log ("The event cycle has been deactivated");
}
}
- C# vs Java
그리고 본격적으로 C#이 기존에 배웠던 Java와 어떻게 다른지를 고찰해보기 위해 여러 예시들을 가져왔다.
In case of "Declaring a class, field, and method" (Class, Field, 그리고 Method를 선언할 때)
C#
public class MyClass : ParentClass, Interface
{ // Class 선언
public int field = 1; // field 선언
public void SomeMethod() // Method 선언
{
//..
}
}
Java
public class MyClass
extends ParentClass
implements Interface
{
public int field = 1;
public void someMethod () {
//..
}
}
Basic Types (Type들을 이용하여 Variable들을 선언할때)
C#
int count = 10;
bool flag = true;
float radius = 4.5f;
double exact = 4.5;
string hello = "Hello";
int[]ages = new int [10];
Java
int count = 10;
boolean flag = true;
float radius = 4.5f;
double exact = 4.5;
String hello = "Hello";
int ages[] = new int [10];
Lists, Sets, Dictionaries (ArrayList, Sets, Dictionaries를 선언하려 할때)
C#
IList <int> = listOfInts = new List <int>();
ISet <float> = setOffFloats = new Hashnet <float>();
IDictionary <string, float> mapStrToFloat = new Dictionary <string, float>();
Java
List <Integer> listOfInts = new ArrayList <Integer>();
Set <Float> setOfFloats = new Hashnet <Float>();
Map <String, Float> mapStrToFloat = new Hashmap <String, Float>();
Properties vs Getters and Setters
C#
public class MyClass
{
private float size;
// define a property with get/set
public float size
{
get
{
return size;
}
set
{
if (value < 0)
{
// throws an error;
}
size = value;
}
}
}
Java
public class MyClass {
private float size;
//define get and set methods
public float getSize() {
return size;
}
public void setSize (float value) {
if (value < 0) {
// throws an error
}
size = value;
}
}
Instantiation and bringing variables from the other class (아까 설명한 Instantiation처럼 다른 클라스에서 variable을 가지고와 값을 할당하는 방법
C#
MyClass obj = new MyClass();
obj.size = 10; // MyClass라는 Class안에 있는 size라는 값을 이번 Class안에서 10으로 저장
obj.size *= 2.5f; // 위와 동일한 값에 2.5를 곱함.
Java
MyClass obj = new MyClass();
obj.setSize (10);
obj.setSize (obj.getSize() * 2.5f);
Array and Lists (배열과 리스트)
C#
int[]array = new int[10];
array[0] = 10;
int size = array.Length;
List <int> list = new List <int>();
list.Add(0);
int i = list[0];
list[0] = i + 1;
int size = list.Count;
Java
int array[] = new int [10];
array[0] = 10;
int size = array.length;
List <Integer> list = new ArrayList <Integer>();
list.add(0);
int i = list.get(0);
list.set(0, i + 1);
int size = list.size();
- C# Style Guide (C# 스타일 가이드)
C#을 작성하는데 있어서 가장 중요한 점은 컴퓨터가 읽을수 있는 동시에 사람도 읽을 수 있어야 한다는 점이다 (Human-readable). 한결같고 일정한 스타일은 가독성을 높이는데 중요한 역할을 한다. 특히 직장에서 팀으로 일하는데 있어서 특정 스타일 가이드가 있다는 점은 매우 흔하다.
- Naming Conventions (이름 설정 관례)
- Code Formatting (코드 포맷)
- Commenting (코멘팅, 주석)
2. Bahaviour-based Programming & Modularity
- GameObject
유니티는 기본적으로 GameObject이라는 Class로 모든 Object들이 실행되는데 GameObject Class는 기본적으로 Object의 이름 설정과 Instantiation and Destruction (인스턴스화와 파괴) 그리고 Accessing the components of the object (오브젝트 컴포넨트 설정)과 같은 속성과 방법들을 실행해준다.
- Components
GameObject안에 내장되어있는 components들은 추가 기능들을 실행하게끔 해준다. 대표적인 것들은 아래와 같다. 유니티에서는 Inspector이라는 탭에서 확인할 수 있다.
Transform | Position(위치), Rotation(회전), Scale(크기)를 담당 |
RigidBody | Physics(물리)를 담당 |
SpriteRenderer | Drawing a sprite (스프라이트 세부 설정)을 담당 |
BoxCollider | Calculating collisions (충돌 계산)을 담당 |

- Access Modifier (접근 지정자)
접근 지정자에는 크게 Public과 Private으로 나눠진다.
Public Fields:
Public Field로 지정된 variable들은 유니티 인스펙터 안에서 조절 가능하게끔 만들어 줄 수 있다. (Tunable Parameter)
Private Fields:
Private Field로 지정된 variable들은 유니티 인스펙터 안에서 조절 가능하지 못하게 하고 오직 스크립트 안에서만 값을 수정 하게 끔 할 수 있다. Debug Mode를 통해 인스펙터에서 확인할 수는 있지만 여전히 수정은 불가하다.
SerializeField:
[SerializeField]가 Private Field로 지정된 variable앞에 입력되면 Private인 상태에도 불구하고 유저가 유니티 인스펙터 안에서 조절 가능케끔 한다.
public float fireCoolDown = 1.0f; // seconds
private float fireTimer = 0.0; // seconds
- Accessing other components - GetComponents
MonoBehaviour이라는 부모 Class를 통해 해당 스크립트가 적용된 GameObject안의 Component를 불러와 값을 설정해 줄 수도 있다.
void start () {
// Rigidbody라는 Component를 GetComponent<>()으로 불러와 variable rigidbody라는 형태로 설정
Rigidbody rigidbody = GetComponent <Rigidbody>();
rigidbody.mass = 10;
}
이때 GetComponent Method는 무거운 쪽에 속하여 유니티에서 게임을 실행할때 딱한번만 GetComponent Method가 실행되게끔만 설정해주는 것이 권장된다.
public class FlyAndShoot : MonoBehaviour
{
private Rigidbody rigidbody;
void Start()
{
rigidbody = GetComponent <RigidBody>(); // 처음 Rigidbody Component를 한번만 불러옴
}
void Update()
{
rigidbody.AddForce (Vector3.up); // 매 프레임에 rigidbody안에 있는 addforce값을 업데이트 (vector에서 (0, 1, 0)값으로 업데이트됨)
}
}
- RequireComponents
만약 스크립트가 GameObject안에 존재하지도 않은 Component를 불러올려고할때는 Runtime Error가 뜬다. 이를 방지하기 위해 RequireComponent를 Class위에 적어두게 되면 게임을 실행할때 GameObject안에 없는 Component가 자동으로 생성되는 것을 볼 수 있다.
[RequireComponent(typeof(Rigidbody))] // GameObject안에 Rigidbody Component가 없어도 게임 실행후 억지로 불러오게 함
public class FlyAndShoot : MonoBehaviour
{
private Rigidbody rigidbody;
void Start()
{
rigidbody = GetComponent <RigidBody>(); // 처음 Rigidbody Component를 한번만 불러옴
}
void Update()
{
rigidbody.AddForce (Vector3.up); // 매 프레임에 rigidbody안에 있는 addforce값을 업데이트 (vector에서 (0, 1, 0)값으로 업데이트됨)
}
}

우선 RequireComponent를 입력해주지 않은채로 실행해보았더니 GameObject안에 Rigidbody Component가 인식되지 않아져 이렇게 에러가 뜨는 것을 확인할 수 있었다.

RequireComponent를 입력해준 상황.

- Conclusion
- 모든 조절 가능한 파라미터들은 public field (or SerializeField가 적용된 private field)로 지정하여 디자이너들이 인스펙터에서 쉽게 값을 조절 할 수 있도록 만들어라
- 모든 내부의 값들은 private field로 만들어 디자이너들이 접근하지 못하도록 만들어라
- GameObject안에 있는 Component들을 불러오기 위해서는 GetComponent method를 사용해 component를 instantiation하여 사용하여라
- 항상 불러오려는 component가 있을때 field에서 private상태로 선언을 먼저 해준다음 GetComponent method는 Start() 안에서 불러와 한번만 불러와 지게끔 세팅하여라. 그리고 instance로된 component 안의 값들은 Update()에서 설정해도 좋다.
- 만약 코드가 GameObject안에 불러오려는 해당 Component가 없어도 억지로 불러오게끔 하려면 RequireComponent를 Class위에 입력하여 불러와라
3. Event-based programming
- Game Loop
아까 전에 Start(), Update()와 같은 Event Cycle에 대해서 알아본 것과 같이 유니티는 지정된 각기 Method의 순서가 있다. 이를 알아보기 위해 Game Loop에 대해서 살펴보겠다.
Initialise game state -> Read input -> Update game state -> Redraw the scene (여기서 색칠된 부분은 전부 하나의 프레임 안에서 이루어진다)
Game Loop은 MonoBehaviour에게 언제 Script를 작동시킬지, Update를 할지, draw를 할지 전부 전달한다. 이는 기존에 프로그래밍 방식과 다르며 Event-based programming이라고 불러진다.
eg) Non-event Programming (while loop을 사용하여 계속 진행되게끔 하였다)
// moving a ship
void Main()
{
Vector2 position = Vector2(0, 0); // (0, 0)값을 Vector2 variable position에 저장
bool crashed = false;
float speed = 2;
while (!crashed) // boolean crashed문이 true가 아닌이상
{
Vector2 joystick = ReadJoyStick(); // Joystick 키 방향으로
position += joystick * speed; // 해당 speed로 포지션을 계속 변경
Drawship (position);
crashed = TestCrashed (position);
}
Explode(); // crashed가 참이면 Explode()로 ship을 폭발
}
eg) Event-based Programming (Update()를 사용하여 계속 진행되게끔 하였다)
// parameters in public fields
public float speed = 2; // meters per second
// state stored in private fields
private Vector2 positions;
void Start() { // At the first scene of the execution
position = Vector2(0, 0); // variable position값을 (0, 0)으로 설정
}
void Update() { // during the execution when every scene updates
bool crashed = TestIfCrashed(position); // 특정 position값에 위치하였을때 crashed가 참이 되면
if (crashed) // 만약 crashed가 참이라면
{
Explode(); // ship은 그대로 폭발
}
else // 그게 아니라면 crashed가 거짓이라면
{
Vector2 joystick = RealJoyStick(); // Joystick의 키 방향으로 위치 조절
position += joystick * speed; // 해당 스피드로 position값 변경 이동함
}
}
- Frame Rate
Frame Rate에서 Update()의 속도는 보통 실행된 가장 마지막 프레임에서 얼마나 많은 시간이 지났는지에 의존한다.
-> 1프레임 후.... 1프레임 후... 1프레임 후.... 반복
그러므로 Player가 game안에서 position 즉 위치를 바꾸며 움직일때 Update()에서 실행된 프레임 사이 얼마나 시간이 흐르는지의 Player속도가 결정난다. 이때 프레임 사이의 시간을 유니티는 Time class로써 관리하는데 이 Class도 당연히 (MonoBehaviour)소속임으로 Instantiation필요 없이 script에 불러올 수 있다.
- 이때 Time.deltaTime은 프레임 후 얼마나 많은 시간이 지나는지 정보를 담아주는 Time Class의 내부 속성이다.
public float accelerate = 1f;
private float speed = 0.01f; // meters per second
void Update()
{
// speed는 acceleration과 함께 가속붙어 빨라지는데
//.. 이때 Time.deltaTime을 함께 곱해주게 되면 프레임이 흐를때마다 원활하게 해당 Object가 천천히 흘러가게끔 해준다.
speed = speed * acceleration * Time.deltaTime;
// 이동 method인 transform.Translate (Inspector에서 흔히 볼 수 있는 transform component)
transform.Translate (speed * Time.deltaTime * Vector3.right);
}
이때 Time.deltaTime을 사용하여 위에서 작성했던 코드를 이렇게 바꿔줄 수 있다. 이는 프레임이 흐를 수록 Object의 변화가 원활하게 천천히 흘러가게끔 도와준다. 이는 컴퓨터 사양의 따라 속도가 다 제각기 다르면 게임을 플레이 함에 있어 문제가 생기므로 fps가 적으면 (컴퓨터 사양이 안좋으면) Time.deltaTime (프레임 사이 시간) 값이 커져 곱했을때 GameObject의 스피드를 더욱 빠르게 만들어 줄 수 있으며, 만약 fps가 많으면 (컴퓨터 사양이 좋으면) Time.deltaTime (프레임 사이 시간) 값이 작아져 곱했을때 GameObject의 스피드를 더욱 느리게 만들어 줄 수 있다. 그래서 어느곳에서 플레이 해도 컴퓨터 사양에 바뀌지 않는 게임의 원활한 진행을 도와줄 수 있는 값이다.
void Update() {
bool crashed = TestIfCrashed(position);
if (crashed)
{
Explode();
}
else // 그게 아니라면 crashed가 거짓이라면
{
Vector2 joystick = RealJoyStick();
// time.deltaTime이 곱해졌으므로 프레임이 흐를수록 보다 천천히 Object의 변화를 생성
position += joystick * speed * Time.deltaTime; // 해당 스피드로 position값 변경 이동함
}
}
원활한 게임진행을 위해서라면 게임은 45fps (Frames per Second)로 진행되어야 하며 30fps부터는 고르지 못하게 된다.
이 Frame Rate에 영향을 미치는 것들은
- 머신 스피드 (CPU, GPU, etc)
- Game Update의 복잡성 (Update()안에 있는 코드의 복잡함)
- Scene render 속도
- 메모리 등이 있다.
- Unity Event Cycle
유니티는 우선적으로 여러 게임에서 생기는 모든 일들을 실현시키기 위해 어마어마한 양의 이벤트들을 실행한다. Event Cycle은 앞서 언급한 것처럼 Start() -> Update() -> OnMouseDown() -> OnCollisionEnter() 와 같은 순서로 진행된다. 이런 설명은 일부분에 불과하고 Event Cycle Loop은 어마무시하게 크다.
그러므로 자세한건 아래 링크에서 찾아 볼 수 있다.
https://docs.unity3d.com/Manual/ExecutionOrder.html
Unity - Manual: Order of execution for event functions
Instantiating Prefabs at run time Order of execution for event functions Running a Unity script executes a number of event functions in a predetermined order. This page describes those event functions and explains how they fit into the execution sequence.
docs.unity3d.com
OnMouseDown | 유저가 Object를 클릭했을때 실행되는 Method |
OnCollisionEnter | Object가 다른 Object와 충돌했을때 실행되는 Method |
또한 기본적으로 여러종류의 Behaviour가 있을때 어느 무엇을 먼저 실행시킬지 유니티 Game Loop은 보장하지 않는다. 이때 Project Setting에서 Script Execusion Order을 통해 어느 Script가 먼저 실행될지 즉 Order 차례를 지정해 줄 수 있다.
Conclusion
- 각각의 MonoBehaviour은 각기 입력된 Game state들을 따로따로 실행시킨다.
- Game State은 private field로 저장된다.
- Start()는 game loop을 먼저 스타트를 끊는 method로써 가장 먼저 실행된다.
- Update()는 안에 들어간 정보들을 진행시키고 프레임이 진행될때마다 값을 업데이트 시킨다.
- Update()의 실행속도가 빨라지기 위해선 fps가 커야 한다. 즉 1초에 실행되는 frame의 수가 많아야 게임 플레이어가 원활한 느낌을 받을 수 있다.
- Other Events Start()나 Update()뿐만이 아닌 다른 Event를 담당하는 method를 class안에 입력하여 실행시켜 줄 수 있다.
4. Good Coding Practices
항상 Code smell이 풍기는 코드를 작성하는 것을 무조건 적으로 주의해야 한다. 만약 코드를 디자인하고 구조화하는데 있어 영 좋지 않다면 이는 나중에 심각한 문제를 야기시킬 수 있다.
Designer-Friendly Code
항상 코드를 짤때는 디자이너가 쉽게 게임을 디자인 할 수 있게끔 세팅해주어야 한다. 이는 마치 TWA님께서 후디니 VEX를 강의 하실 때 알려준 것과 동일 한 것 같았다. 후디니에서 ch()을 이용하여 항상 Control 노드에 파라미터로 각기 세팅해주는 방식이 유니티에도 일맥상통한다.
- 항상 조절 가능한 파라미터들은 Inspect에서 보이도록 Variable에 [SerializeField]를 미리 입력해주어라
- 파라미터에 디자이너가 이해할 수 있는 이름을 붙여라
- 디자이너가 쉽게 이해할 수 있는 단위로 파라미터 값을 조절 가능하게 하여라
- 난해한 숫자나 코드를 요구하지 말고 UI (User Interface)를 통해 최대한 디자이너가 조절할 수 있는 수는 그래픽으로 나타나게 하여라.
첫날 부터 이렇게 배우는게 많았다니... 이러니 다른 과목하고 공부하면서 뒤쳐질 수 밖에 없다... 하아아 코딩 너무 어렵다....
p.s.
작업을 하다보니 햇갈리는 것들이 있어서 까먹기 전에 여기다가 적어보기로 하였다. 우선 스크립트 안 Class에서 MonoBehaviour Class를 부모 Class로 받았을때 어떤 Objects나 Methods를 사용할 수 있을까? (즉 MonoBehaviour 소속의 Objects와 Methods는 무엇들이 있을까?) 이는 이 링크안에 잘 적혀있는 것들을 볼 수 있다. 우선 MonoBehaviour은 Component라는 Class를 불러오는 방식 같다. 이를 통해 Component class안에 있는 기본적인 속성들을 불러올 수 있는데 이는 아래와 같은 Objects들이 포함된다.
https://docs.unity3d.com/ScriptReference/MonoBehaviour.html
Unity - Scripting API: MonoBehaviour
docs.unity3d.com
이때 대표적인 것 들만 살펴보고 가겠다.
Objects:
gameObject (Object from the GameObject class) | Component가 연결된 gameObject. Scene에서 모든 Object의 기본 Class라 이해하면 된다. |
transform (Object from the Transform class) | gameObject의 위치/회전/크기들을 설정해기 위한 property. (inspector에서 보면 transform도 component이지만 스크립트 작성때 만큼은 script안에서 object로 지정된 class로서 다뤄진다) |
이때 gameObject과 transform은 기존에 각기 그들만의 Variable즉 속성들과 Methods를 가지고 있는 Class였는데 MonoBehavoiur에서 Object로 변환되어 MonoBehaviour은 그들의 모든 정보를 상속받았다고 볼 수 있다. 그말은 MonoBeviour Class가 또한 GameObject과 Transform이라는 Class를 부모로 삼아 특정 Properties를 물려받아 MonoBehaviour을 또한 부모로 삼는 현재 Class에서 Instatiation을 필요로 요하지 않게 되는 것이다.
그런데 Time Class 같은 경우는 Object로 만들지 않고도 Time.deltaTime과 같이 바로 현제 스크립트에서 불러오는 것이 가능하다. 분명 Time 자체는 Class인데... MonoBehaviour안에서 상속받지도 않았던 것 같다... 그것에 대해서는 계속 공부해보면서 이해해보기로 하였다.

MonoBehaviour Class로 불러올 수 있는 대표적인 Methods는 다음과 같다.
Methods:
여러가지 Event Cycle Methods | Event-based programming을 담당하는 여러가지 Methods. 위에 설명을 해놓았으니 넘어가겠다. |
GetComponent<>() | Component를 불러오는 Methods. 이를통해 Component안의 속성을 현제 Script안에서 조절할 수 있게끔함. GameObject안에 지정되어있는 component만 성공적으로 불러올 수 있다. * 예외: [RequireComponent] |
Destroy() | 특정 Object를 없애 버리는 Methods. 파라미터로는 없애버릴 Object과 파괴시키기 전 시간을 집어넣을 수 있다) |
FindObjectOfType() | 활성화된 모든 gameObject의 데이터 타입을 검색한다. 파라미터로는 Type이 들어간다 |
Instantiate() | 특정 Object을 복사하여 반환하는 Methods. 파라미터로 주로 복사할 Object, 위치 그리고 회전이 들어간다. |
차츰차츰 이렇게 정리를 하다보니 왜 유니티가 Object-Oriented Programming (객체 지향 프로그래밍)으로 이루어져 있다는게 이제야 실감이 가기 시작한다. Class를 상속 받고 Instantiation시켜 또 상속받고 대물림 되니 햇갈려 지는 부분이 많아졌던 것 같다. 계속 공부를 진행하면서 OOP에 대해 이해도도 높여야 겠다는 생각이 든다.