Tutorial (8) 

스크립트 작업 기초

 

작성 기준 버전 :: 2019.2

 

[본 튜토리얼의 내용을 유튜브 영상을 통해서 확인하실 수도 있습니다]

 

이번 섹션에서는 스크립트 작업으로 기초적인 커스텀 컴포넌트를 만드는 법을 배워보자.

 

본격적인 섹션 진행에 앞서 게임 오브젝트와 컴포넌트에 관련된 지식이 필요하다면 이 포스트를 참고해보자.

 

또한 이번 섹션을 진행하기 위해서는 C# 프로그래밍에 대한 기초적인 지식을 필요로 한다.

 

커스텀 컴포넌트 생성

 

[그림 1]

 

우선 커스텀 컴포넌트를 만들기 위해서 C# 스크립트를 하나 생성해보자. 프로젝트 뷰에 우클릭하여 [Create > C# Script] 항목을 선택한다.

 

 

그렇게하면 NewBehaviourScript라는 이름으로 C# 스크립트 파일이 하나 생성된다.

 

 

바로 엔터 키를 누르지 말고 파일의 이름을 ScriptingTest로 변경하고 엔터 키를 누르도록 하자. C# 스크립트 파일은 제일 처음 이름이 정해질 때, 스크립트 파일 내부의 클래스 이름이 정해지며, 스크립트 파일의 이름과 클래스의 이름이 일치하는 것을 권장하기 때문에 클래스의 이름을 처음에 제대로 정하는 것이 나중에 수정하는 것보다 좋다. 특히 나중에 파일의 이름을 바꾸면 내부의 클래스의 이름도 수동으로 바꿔야하므로 굉장히 번거롭다.

 

 

그리고 생성된 스크립트 파일을 더블클릭하면 비주얼 스튜디오가 열립니다.

 

모노비헤이비어 클래스 상속

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ScriptingTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

 

최초로 생성된 기본 코드는 위와 같다. 먼저 생성된 ScriptingTest 클래스가 모노비헤이비어(MonoBehaviour) 클래스를 상속받고 있는 것을 볼 수 있다. 이 유니티로 게임을 제작할 때 사용되는 C# 클래스는 이 모노비헤이비어를 상속받는 클래스과 상속받지 않는 클래스로 크게 나누어진다.

 

 

모노비헤이비어 상속 여부에 따른 차이는, 모노비헤이비어를 상속받지 않은 클래스는 게임 오브젝트에 컴포넌트로써 부착되지 못한다는 것에 있다. 때문에 컴포넌트로써 게임 오브젝트에 부착되어서 씬 내부에 존재해야하는 클래스는 모노비헤이비어를 상속받는게 필수이고, 씬에 컴포넌트로 배치되지 않고 코드 내부에서 개념적으로만 존재할 클래스는 모노비헤이비어를 상속받지 않아야 한다.

 

모노비헤이비어의 라이프 사이클

 

 

 

 

모노비헤이비어를 상속받아서 게임 오브젝트에 부착되어 동작하는 스크립트를 잘 활용하려면 모노비헤이비어의 라이프 사이클에 대해서 잘 알아두는 것이 좋다. 모노비헤이비어를 상속받는 컴포넌트는 생성되어 게임 오브젝트에 부착되는 순간부터 위의 이미지와 같은 과정을 거친다.

 

그리고 위의 모노비헤이비어 상속 파트에서 본 코드 블럭을 보면 Start() 함수와 Update() 함수가 구현되어 있는 것을 볼 수 있다. 이와 같이 거치는 과정의 이름으로 함수를 만들어두면 해당 과정을 거칠 때, 그 함수가 실행되는 구조이다.

 

그럼 각 과정이 언제 호출되는지 어떻게 구현하면 되는지에 대해서 하나씩 알아보자.

 

Awake

 

private void Awake()
{
    Debug.Log("Awake");   
}

 

Awake 과정은 스크립트 인스턴스가 로딩될 때 단 한 번 호출되는 함수이다. 컴포넌트에 대한 초기화가 필요한 경우에 사용된다. 참고로 모노비헤이비어를 상속받는 클래스는 생성자 대신에 Awake() 함수를 구현해서 사용해야 한다.

 

OnEnable

 

private void OnEnable()
{
    Debug.Log("OnEnable");   
}

 

OnEnable 과정은 모노비헤이비어를 상속받은 컴포넌트가 부착된 게임 오브젝트가 활성화될 때마다 호출되는 함수이다.

 

 

에디터의 씬에서 게임 오브젝트를 선택하면 인스펙터 뷰에서 선택한 게임 오브젝트에 대한 정보를 볼 수 있는데, 이 중에 게임 오브젝트 이름 앞에 체크박스가 있다. 이 체크박스를 클릭해보면 체크박스 상태에 따라서 게임 오브젝트가 활성화되었다 비활성화되었다하는 것을 볼 수 있다. 이렇게 게임 오브젝트가 활성화될 때마다 OnEnable() 콜백 함수가 호출되는 것이다. 참고로 게임 오브젝트가 비활성화된 상태에서는 해당 게임 오브젝트에 부착된 모든 컴포넌트가 동작을 멈춘다.

 

Start

 

private void Start()
{
    Debug.Log("Start");   
}

 

Start 과정은 Update 과정이 실행되기 직전에 단 한 번 호출된다. 모노비헤이비어의 라이프 사이클 중에 단 한 번 호출된다는 점이 Awake와 같지만 Start는 게임 오브젝트가 활성화된 경우에만 호출된다는 차이점이 있다.

 

Update

 

private int i = 5;
private void Update()
{
    i--;
    if(i >= 0)
    {
        Debug.Log("Update :: " + i);
    }
    else
    {
        Destroy(gameObject);
    }
}

 

Update 과정은 모노비헤이비어가 활성화된 상태에서 매 프레임마다 호출된다. 대부분의 게임의 동작 처리는 이 Update() 함수에서 수행되는 경우가 많다. 다만, 이 Update() 함수는 프레임마다 호출되기 때문에 프레임 드랍이 발생하는 경우에는 호출 횟수가 줄어든다. 프레임과 상관 없이 코드가 작동하기 원한다면 FixedUpdate() 함수를 사용해야 한다.

 

Update() 함수는 OnEnable() 함수를 설명하면서 이야기했듯이 게임 오브젝트가 비활성화된 상태에서는 동작하지 않는다.

 

LateUpdate

 

private void LateUpdate()
{
    Debug.Log("LateUpdate");   
}

 

LateUpdate는 단어 그대로 늦은 업데이트로 Update() 함수가 실행된 직후에 실행되는 업데이트 함수이다. Update() 함수에서 게임 로직을 처리한 직후에 처리하고 싶은 로직이 있다면 이곳에서 처리하면 된다.

 

FixedUpdate

 

private void FixedUpdate()
{
    Debug.Log("FixedUpdate");   
}

 

FixedUpdate는 매 프레임마다 호출되는 Update와 달리 지정된 시간마다 호출되는 업데이트 함수이다. 때문에 프레임이 들쭉날쭉한 상황에서도 일정한 시간마다 호출된다. 주로 호출 시간에 따라서 결과가 달라지면 안되는 물리적인 계산에 사용된다.

 

OnDisable

 

private void OnDisable()
{
    Debug.Log("OnDisable");   
}

 

OnDisable 과정은 모노비헤이비어가 비활성화되는 경우에 사용된다. 그리고 오브젝트가 삭제되는 경우에도 호출된다.

 

OnDestroy

 

private void OnDestroy()
{
    Debug.Log("OnDisable");   
}

 

OnDestory 과정은 모노비헤이비어가 제거될 때 호출된다.

 

 

위의 코드를 모두 ScriptingTest 클래스에 작성하고 플레이시켜보면 위의 이미지와 같은 순서로 로그가 발생하는 것을 볼 수 있다.

 

 

 

 

 

변수

 

우리가 게임을 만들면서 사용될 값, 공격력, 방어력, 공격속도, 이동속도, HP 등의 데이터나 정보를 담아둘 것을 변수라고 부른다. 유니티 엔진에서 스크립트를 작성하는 C#은 담고자하는 값의 종류에 따라서 변수의 종류가 나누어진다. 그럼 이 변수의 종류에 대해서 알아보도록 하자.

 

정수(int)

 

int i = 10;

 

첫 번째 변수 유형은 정수형이다. 정수형 변수 int는 0과 양의 정수, 음의 정수를 담기 위한 변수로, -2,147,483,648부터 2,147,483,647까지 담을 수 있다. 

 

남아있는 라이프의 갯수, 현재 생산된 인구 수 등의 정수로 딱 떨어지는 곳에서 사용될 수 있다.

 

실수(float)

 

float f = 3.14159f;

 

두 번째 변수 유형은 실수형이다. 실수형 변수 float은 소수를 담기 위한 변수로 일반적으로 소수점 다섯 번째자리 0.00001까지 정확도를 표현할 수 있다.

 

주로 1.2초 같은 시간이나 20.25%와 같은 확률 등을 표현할 때, 주로 사용된다.

 

문자열(string)

 

string str = "hello";

 

세 번째 변수 유형은 문자열입니다. 문자열 변수 string은 말그대로 문자들의 집합인 문자열을 담는 변수이다.

 

주로 캐릭터나 아이템의 이름, 설명, 게임에서 사용되는 대사 자막 등의 데이터를 담는데 사용된다.

 

논리값(bool)

 

bool isMoveable = true;

 

네 번째 변수 유형은 논리값이다. 논리값 변수 bool은 참(true) 혹은 거짓(false)의 상태를 가지는 변수로 주로 조건을 처리할 때 사용된다.

 

이 외에도 각 종류의 변수를 묶음 단위로 취급하는 배열 등이 있고, 일반 C# 클래스나 모노비헤이비어를 상속받은 클래스 역시 변수가 될 수 있다.

 

 

함수

 

함수는 게임 기능을 수행하기 위한 작업을 하나의 블록으로 묶은 것을 의미한다. 모노비헤이비어의 라이프 사이클에 대해서 설명하면서 본 Awake, OnEnable, Start, Update, OnDisable, OnDestroy 역시 함수이다. 일반적으로 함수는 하나의 기능 단위로 작성되는 경우가 많다.

 

int attackDamage = 10;

public bool Attack(Monster monster)
{
    monster.hp -= attackDamage;
    return monster.hp <= 0;
}

 

위의 예시 코드는 몬스터를 공격해서 체력을 공격력만큼 깎고, 몬스터의 체력이 0 이하가 되면 true를 반환하도록 코드가 작성되어 있다. 이렇게 하면 Attack() 함수를 호출하여 몬스터의 체력을 깎고 공격한 몬스터가 죽었는가에 따라서 여러가지 처리를 할 수 있게 된다.

 

 

공개 수준 결정

 

개발자는 코드를 작성하면서 변수나 함수에 대해서 공개 수준을 결정할 수 있다.

 

public int i;

protected float f;

private string str;
 
public void Function1() { }
 
protected void Function2() { }
 
private void Function3() { }

 

변수와 함수의 공개 수준은 앞에 표시된 public, protected, private 키워드를 통해서 결정된다. 이러한 공개 수준은 일반적인 C# 프로그래밍에서와 같이 public은 클래스 외부에서 접근이 가능하고 protected는 해당 클래스를 상속받은 클래스에서만 접근이 가능하다. 그리고 private는 해당 클래스의 내부에서만 사용 가능하다.

 

public class ScriptingTest : MonoBehaviour
{
    public int attackDamage = 10;
}

 

그리고 유니티 엔진만의 특징으로는 모노비헤이비어 클래스를 상속받은 클래스에서 public으로 설정된 변수는 에디터의 인스펙터 뷰에서 바로 보고 수정할 수 있다는 장점이 있다.

 

 

이러한 방식의 장점은 매번 게임의 수치가 바뀔 때마다 프로그래머가 코드를 수정하고 새로 빌드 과정을 거칠 필요없이 게임 디자이너가 에디터에서 즉석으로 값을 바꿀 수 있다는 것이다.

 

하지만 인스펙터 뷰에서 보이게 하고자 하는 모든 변수를 public으로 설정하면 코드 내부에서 어떤 클래스에서던지 접근이 가능해진다. 이런 경우를 방지하고자 protected나 private로 설정한 채로 인스펙터 뷰에 공개하고 싶을 수도 있다.

 

[SerializeField]
private int attackDamage = 10;

 

그럴 때는 SerializeField라는 어트리뷰트를 해당 변수 앞에 명시해주면 private나 protected로 둔 상태로도 인스펙터 뷰에 변수를 공개할 수 있다.

 

[HideInInspector]
public int attackDamage = 10;

 

그와 반대로 변수를 public으로 둔 상태로 인스펙터 뷰에 공개하고 싶지 않다면 HideInInspector 어트리뷰트를 붙여주면 된다.

 

모노비헤이비어 클래스를 상속받아서 만들어진 컴포넌트는 클래스를 기반으로 변수를 어떻게 구성하고 함수를 어떻게 구현하느냐에 따라서 그 컴포넌트의 기능과 역할이 정해진다. 

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

읽기 좋은 코드를 위한 간단한 원칙

 

 

 

프로그래머들 사이에선 이런 농담들이 있다.

 

이 코드가 무슨 코드인지는 오직 신과 나만이 안다.

그리고 이제는 오직 신만이 아신다.

 

이게 무슨 의미인가 하면, 작업할 당시에는 내가 아는 코드였지만 시간이 지나면 자신도 본인의 코드를 이해하지 못하게 되는 경우가 있다는 뜻이다. 혼자서 하는 작업도 이럴진데, 타인과 하는 작업은 어떨까? 사람마다 코드 작성 타입은 각양각색이라 다른 사람의 코드를 읽어내기가 몇 배는 더 힘들어진다.

 

특히 혼자서 프로그래밍을 공부하던 사람이 다른 학생와 조별 프로그래밍 과제를 한다거나, 다른 개발자나 협업을 하게 되었을 때 자신의 평소 코딩 스타일과 다른 스타일의 코드를 만나게 되면 격렬한 동공지진을 일으키게 된다. 거기에 주석까지 없다면 혼란은 가중된다.

 

그럼 이제 읽기 좋은 코드를 만들기 위한 고민을 시작해보자.

 

 

코드 블럭 중괄호 스타일 { }

 

프로그래머들 사이에서 가장 뜨겁고 격렬한 이슈인 중괄호 문제부터 들어가보자. 중괄호 스타일 문제는 해외에서는 SVN[각주:1]에 코드 전체가 커밋되었길래 봤더니 중괄호 스타일을 전부 바꾸고 커밋했더라 "작업자를 찾아서 가만두지 않겠다" 라는 밈이 나올 정도이다.

 

이 문제는 크게 두 가지 계파로 나누어지는데 다음과 같다.

 

조건문이나 반복문 아래로 중괄호를 내리는 스타일 :

 

if (bCondition)
{
    // Todo
}
else
{
    // Todo
}

for (int i = 0; i < length; i++)
{
    // Todo
}

while (bCondition)
{
    // Todo
}

 

조건문이나 반복문 옆으로 중괄호를 붙이는 스타일 :

 

if (bCondition) {
    // Todo
} else {
    // Todo
}

for (int i = 0; i < length; i++) {
    // Todo
}

while (bCondition) {
    // Todo
}

 

아래로 중괄호를 내리는 스타일의 경우, 줄 수는 늘어나지만 코드 블럭의 시작과 끝을 명확히 알 수 있다는 장점이 있고, 옆으로 붙이는 스타일은 코드 시작과 끝을 명확하게 보기는 어렵지만 코드의 라인 수가 줄어서 한 눈에 더 많은 코드를 볼 수 있다는 장점이 있다.

 

사실 이 문제는 어떤 것이 옳다라고는 단정내려버리기 어려운 문제이지만, 딱 한 가지 나쁜 경우가 있다. 바로 이 두 가지 스타일을 섞어서 쓰는 것이다. 이 두 가지 스타일을 섞어서 쓰는 경우, 개발자들이 코드 블럭의 시작점을 쉽게 놓치게 되는 경우가 상습적으로 발생할 것이다. 이런 경우가 자주 발생한다면, 조건문이나 반복문 내부에 코드를 넣으려다 밖에다 써버린다든지, 그 반대의 경우가 쉽게 발생하고, 두 스타일을 사용하는 모든 개발자가 함께 고통받는 헬코딩이 열린다.

 

그렇기 때문에 이 중괄호 스타일 문제는 일반적으로 팀 내에서 다수를 차지하는 파의 스타일을 따라가든지, 아니면 프로젝트 관리자가 익숙한 스타일을 따라가게 된다.

 

조건문에 관련해서 중괄호 스타일 문제는 하나 더 있다. 그것은 조건문 내부 코드가 한 줄일 때, 중괄호를 생략하는 스타일이다.

 

if (bCondition)
{
    // Todo
}
else
{
    // Todo
}
if (bCondition)
    // Todo
else
    // Todo

 

컴파일러는 조건문 내부 코드가 한 줄일 때, 중괄호를 생략하는 것을 허용하는데, 다른 스타일은 대부분 취향에 따라 갈리는 것이라 팀의 규칙에 따라 정해진 스타일을 따라가기를 권하지만 이 스타일만큼은 반드시 지양하라고 권하고 싶다.

 

그 판단의 근거는 한 줄 짜리 조건문을 치면서 중괄호 { } 두 번을 안치고 "라인 두 줄을 아꼈다.", "코드 치는 속도가 더 빠르다"는 자그마한 이점을 취하기에는 그로 인해서 발생할 문제의 리스크가 훨씬 크기 때문이다. 이런 스타일은 조건문 내부에 코드를 추가할 일이 발생하면 생략했던 중괄호를 다시 추가해야할 뿐만 아니라, 중괄호를 추가하면서 라인을 이리저리 재정렬해야하는 번거로움이 발생하고, 정말로 기초적인 실수지만 조건문 블럭 안에 들어가야할 코드를 블럭 밖으로 빼버리게 만드는 실수를 일으키게 만드는 경우가 잦다.

 

그리고 이 문제는 신텍스 에러가 아닌 논리적인 버그를 발생시킬 확률이 매우 높으며 발견하기가 굉장히 까다로울 확률이 높다. 앞서 이야기 했듯이 조건문 블럭 안에 들어가야할 코드를 블럭 밖으로 빼버리는 실수는 간단한 실수지만, 인간은 무언가 문제를 찾을 때, 자신이 당연한 곳에서 실수하지 않을 것이라는 가정을 하는 경향이 강하기 때문에 문제를 찾으려고 코드를 훑는 와중에 이 "간단한 실수"를 몇 번이나 스쳐보고 지나갈 확률이 높다. 그리고 문제를 찾다 찾다 못찾아서 코드를 한 줄씩 검토하면서 나아갈 때 결국에 이 문제를 마주치게 된다.

 

 

 

네이밍 스타일

 

두 번째 이슈는 바로 변수와 함수, 클래스 등의 네이밍 스타일이다. 변수와 함수의 이름은 간단하게는 각 변수와 함수의 구별을 넘어서 무엇을 담는 변수인지, 무슨 일을 처리하는 함수인지를 알려주는 역할을 한다. 프로그래머는 개발을 진행하면서 무수히 많은 변수와 함수, 클래스 등을 만들며, 그 이름을 정해야 한다. 오죽하면 프로그래머의 가장 큰 고민이 변수 이름을 짓는 것이라는 말이 나왔겠는가?

 

절대 하지 말아야할 네이밍 스타일

 

중괄호를 다루는 법은 일반적으로 기호에 가까운 것이라 절대 금지한다라고 할 만한 방법이 크게 없지한 이름 짓기에서는 반드시 금지할 게 있다.

 

무성의한 네이밍

 

int i;
float f;
string a, b, c;
void foo();
bool function();

 

변수나 함수의 이름을 무성의하게 대충 짓는 것은 최악의 행위이다. 앞에서 이야기 했듯이 변수나 함수의 이름은 그 변수가 어떤 값을 담을 것인지, 그 함수가 어떤 작업을 처리할 것인지를 알려주는 역할을 한다. 하지만 대충 지어진 이름은 이 변수나 함수가 어떤 역할을 할 것인지를 명확하게 알 수 없게 만들기 때문에 코드의 흐름을 파악하기 위해서 모든 코드와 주석을 일일이 읽어야만 되게 만들어버린다. 그 코드를 작성한 본인이라고 하더라도 시간이 지나면 코드의 흐름을 까먹게 될 확률이 매우 높기 때문에 유지보수가 어려워지게 만든다. 

 

그리고 기억하라 int i, int j가 허용되는 곳은 반복문의 인덱스로 사용되는 임시 변수뿐이다.

 

혼란스러운 네이밍

 

bool isCantMove;

 

뜻이 모호하거나 혼란스러운 네이밍 역시 피해야 한다. 특히 bool이나 boolean 같은 논리 변수에서 이러한 혼란을 피해야 하는데, 쉽게 혼란이 발생하는 경우는 논리 변수로 부정을 정의하는 경우이다. 위의 bool 변수의 경우 is Can't Move, 움직이지 못하는가? 를 정의하는데 이를 조건문에서 구현할 때는 아래와 같이 된다.

 

if (isCantMove)
{
    // To do
}

if (!isCantMove)
{
    // To do
}

 

이를 해석해보면 위 조건문은 "움직이지 못하는가?"이고 아래 조건문은 "움직이지 못하지 않는가?"가 된다. 이런 식으로 부정 조건을 정의하면 사람이 해석하는 과정에서 실수가 발생할 수 있다.

 

bool isMoveable;

 

이런 문제를 발생시키지 않기 위해서는 예시처럼 is Moveable, 움직일 수 있는가? 라고 정의하는 게 좋으며, 이는 조건문으로 :

 

if (isMoveable)
{
    // To do
}

if (!isMoveable)
{
    // To do
}

 

와 같이 구현되며, "움직일 수 있는가?", "움직이지 못하는가?"와 같이 자연스럽게 해석된다.

 

한글리쉬 네이밍

 

일부 개발자들은 아는 영어 단어가 많지 않거나 필요한 단어를 찾기 힘들다는 이유로 그냥 한국어 단어를 발음이 나는대로 영어로 적어서 변수를 만든다.

 

string juso;
int oNuelNalJja;
string yoil;

 

이러한 네이밍은 뭔가 굉장히 난독화된 코드처럼 보이며, 코드를 읽는 사람이 발음해보기 전에는 무슨 변수인지 알기가 어렵다. 한글 로마자 표기는 생각보다 읽기 어렵고 불편한 방식이다. 차라리 무슨 단어를 써야될지 모르겠다면 시간을 조금 더 써서 영어사전을 뒤져봐라.

 

권장하는 네이밍

 

변수나 함수, 클래스의 이름을 명확하게 짓는 것만으로도 코드의 가독성은 상당히 올라가며 유지보수 역시 쉬워진다.

 

변수 이름

 

변수 이름을 작성할 때는 기본적으로 명사를 사용한다.

 

int num;

 

숫자를 세는 변수를 예로 들어보자. 이러한 변수에는 num이라는 이름이 쉽게 붙여진다.

 

int count;

 

갯수를 세는 변수라면 조금 더 명확하게 count라는 이름을 써보자.

 

int itemCount;

 

여기서 더 명확하게 무엇의 갯수를 세는 변수인가?를 추가하면 itemCount가 된다.

 

public class Item
{
    public int itemCount;
}

 

하지만 역으로 멤버 변수일 때는 또 다르다. Item 클래스에 같은 아이템을 여러 개 가질 수 있다고 했을 때, 같은 아이템의 갯수를 표현하는 itemCount 변수는 어떤가? 적절해보이는가?

 

Item item = new Item();
ShowItemCount(item.itemCount);

 

하지만 실제로 itemCount 변수를 사용할 때는 item.itemCount로 이름이 중복 표현된다.

 

public class Item
{
    public int count;
}

Item item = new Item();
ShowItemCount(item.count);

 

이럴 때는 count라는 이름만 써줘도 무엇의 갯수인지 명확하게 표현된다.

 

List<Item> itemList = new List<Item>();
itemList.Count;

 

이와 같은 맥락으로 C#에서 리스트를 사용할 때도 리스트 안에 들어있는 요소의 갯수를 반환받을 때, Count라고 하지 ListInElementsCount라고 일일이 길게 변수이름을 정하지 않는 것과 같다.

 

함수 이름

 

함수는 기본적으로 어떤 일을 처리하는 행위이기 때문에 함수가 처리하고자 하는 행위를 이름으로 만드는 것이 기본이다. 이름을 짓는 방법은 동사, 동사+명사, 동사+부연 설명 혹은 반대로 명사+동사, 부연 설명+동사 방식으로 지어진다. 이른바 두괄식이냐 미괄식이냐 하는 것인데, 대부분 영미권에서는 동사가 앞으로 오는 두괄식을 선호한다. 선호 이전에도 두괄식이 이 함수가 어떤 행위를 하는지 빠르게 알 수 있기 때문에 보통은 자주 사용된다.

 

void Run(); // 동사 : 실행한다
void MoveToDestination(); // 동사 + 부연설명 : 이동한다 + 목적지로
void AttackEnemy(); // 동사 + 명사 : 공격한다 + 적을

 

클래스 이름

 

클래스 이름 역시 그 클래스 주로 하는 행위에 따라 지어지며 주로 명사로 이름을 짓는다. 게임에서 플레이어를 컨트롤 하는 클래스라면 PlayerController, 입력을 관리하는 클래스라면 InputManager와 같은 방식이다.

 

public class SendData { }

 

다만, 이런 방식으로 클래스가 처리하는 일을 이름으로 짓다보면 실수로 클래스의 이름을 함수 형식으로 짓는 경우들이 있다. 위의 예시처럼 데이터를 보내는 클래스를 정의하려고 할 때, "이 클래스는 데이터를 보내는 역할을 하니까 SendData로 지어야겠다"라고 하는 경우다. SendData라는 이름은 동사+명사의 형태로 클래스 작명법보다는 함수 작명법에 가까운데 클래스는 행위가 아닌 행하는 객체이기 때문에 이러한 동사+명사의 작명법이 어울리지 않으며, 함수와 헷갈릴 가능성이 크다.

 

public class DataSender { }

 

그렇기 때문에 데이터를 보내는 클래스의 이름을 정의하고자 할 때는 위의 예시처럼 이름을 명사화해서 DataSender, 데이터 전송자와 같이 네이밍해주는 것이 좋다. 

 

 

 

이름 표기법

 

대표적인 이름 표기법

 

중괄호를 다루는 방법에도 여러 가지 방법이 있듯이 변수, 함수, 클래스 이름을 표기하는데도 여러 가지 방법이 있다. 그 중에 대표적인 표기법으로는 카멜 표기법, 파스칼 표기법, 스네이크 표기법이 있다.

 

카멜 표기법(Camel Casing)

 

int itemCount;
float moveDirection;
string errorMessage;

 

카멜 표기법은 변수명으로 사용되는 여러 단어 중에 제일 첫 단어는 소문자로, 그 뒤로 새 단어가 등장할 때마다 그 단어의 첫 문자는 대문자로 표기하는 방법이다. 새로운 단어가 나타날 때마다 대문자가 튀어오르는 모양이 낙타의 등 모양 같다고 해서 카멜 표기법이라고 부른다. 이 방법은 새로운 대문자가 나타날 때마다 끊어 읽으면 되기 때문에 상당히 가독성이 좋은 편에 해당한다.

 

파스칼 표기법(Pascal Casing)

 

int ItemCount;
float MoveDirection;
string ErrorMessage;

 

파스칼 표기법은 카멜 표기법과 비슷하지만 제일 첫 단어의 첫 문자 역시 대문자로 표기한다. 가독성 자체는 카멜 표기법과 비슷하다.

 

스네이크 표기법(Snake Casing)

 

int item_count;
float move_direction;
string error_message;

 

스네이크 표기법은 새 단어마다 언더바( _ )를 삽입하는 형식의 표기법이다. 팟홀 표기법(Pothole Casing)이라고도 불린다. 가독성은 좋은 편에 속하지만 이름이 길어질 수록 넣어야 하는 언더바가 늘어날 뿐만 아니라 언더바를 치는 과정 역시 매우 불편한 면이 많다.

 

그 외의...

 

int nItemCount;
float fMoveDirection;
string strErrorMessage;

 

대표적인 위 세 가지 표기법 이 외에도 여러 가지 표기법이 존재하는데, 변수의 타입을 선행표기하는 헝가리안 표기법(Hungarian Casing)이 있고,

 

int itemcount;
float movedirection;
string errormessage;

 

그냥 모든 문자를 소문자로 표기하는 플레인 표기법(Plain Casing) 역시 존재한다.

 

일반적인 표기법 사용

 

프로그래밍에서 사용되는 표기법은 종류가 매우 다양하다. 하지만 하나의 표기법을 모든 코드 전체에 적용시키는 경우는 없고, 분류에 따라서 적절하게 여러 가지 표기법을 혼합해 사용하는 경우가 대다수이다. 각 분류에 따라 자주 사용되는 표기법은 아래와 같다.

 

클래스와 함수의 이름 표기

 

public class Monster
{
    public void Attack() { }
    public void Move() { }
    public bool FindEnemy() { }
}

 

클래스와 함수의 이름을 표기할 때는 주로 파스칼 표기법을 사용한다. 

 

변수 이름 표기

 

변수 이름의 표기법은 변수의 종류에 따라서 사용하는 방법이 많다.

 

클래스 멤버 변수

 

// Camel Casing
public class Monster
{
    public float moveSpeed;
    public float attackSpeed;
}

// Pascal Casing
public class Monster
{
    public float MoveSpeed;
    public float AttackSpeed;
}

// m_
public class Monster
{
    public float m_moveSpeed;
    public float m_attackSpeed;
}

// _
public class Monster
{
    public float _MoveSpeed;
    public float _AttackSpeed;
}

 

특히 변수에 관련된 쪽에서 표기법이 굉장히 의견이 분분한 편인데, 기본적으로는 카멜 표기법과 파스칼 표기법이 자주 사용되고, 멤버 변수와 다른 매개 변수나 임시 변수와 구분하기 위해 m_나 _를 앞에 붙이는 경우가 많다.

 

함수 매개 변수

 

// 1
public class Monster
{
    public float moveSpeed;
    public void SetMoveSpeed(float moveSpeed) { this.moveSpeed = moveSpeed; }
}

// 2
public class Monster
{
    public float moveSpeed;
    public void SetMoveSpeed(float movespeed) { moveSpeed = movespeed; }
}

// 3
public class Monster
{
    public float moveSpeed;
    public void SetMoveSpeed(float _movespeed) { moveSpeed = _movespeed; }
}

// 4
public class Monster
{
    public float m_moveSpeed;
    public void SetMoveSpeed(float moveSpeed) { m_moveSpeed = moveSpeed; }
}

// 5
public class Monster
{
    public float m_moveSpeed;
    public void SetMoveSpeed(float a_moveSpeed) { m_moveSpeed = a_moveSpeed; }
}

// 6
public class Monster
{
    public float _moveSpeed;
    public void SetMoveSpeed(float moveSpeed) { _moveSpeed = moveSpeed; }
}

 

함수의 매개 변수의 경우 멤버 변수와 이름이 같아서 덮어씌워지는 경우가 많기 때문에 굉장히 많은 방법이 사용된다. 1번 경우처럼 그냥 같은 타입을 사용하고 this 키워드를 사용하는 방법부터, 매개 변수(argument)임을 명시하기 위해서 a_를 붙이는 방법까지 사용되기도 한다. 멤버 변수 표기법과 겹치지 않는 것이 우선이기 때문에 멤버 변수 표기법을 회피한 표기법을 선택한다.

 

함수의 임시 변수

 

public void ChangeMoveSpeed(float movespeed)
{
    float prevSpeed = moveSpeed;
}

 

함수 안에서 생성되는 임시 변수의 표기법은 비교적 자유로운 형태를 띈다. 네이밍 규칙만 정상적으로 지켜지면 알아보기 쉽고, 멤버 변수나 매개 변수와 이름이 겹치지 않게 이름 짓기 쉽기 때문에 표기법에 크게 연연하지 않고 적당하게 작성되는 편이다.

 

 

 

주석(Comment)

 

프로그래밍을 처음 배울 때 주석을 습관적으로 달도록 배우는 경우가 많다. 하지만 과한 주석이 오히려 가독성을 해치기도 한다.

 

불필요한 주석 쓰기

 

// 아이템 리스트를 아이템 리스트 길이만큼 순회한다.
for (int i = 0; i < itemList.Count; i++ /*반복마다 i에 1을 더한다.*/)
{
    var item = itemList[i]; // 이번 반복 횟수의 아이템을 가져온다.
    if (item.type == ItemType.Equipment) // 아이템의 타입이 장비라면 ...
    {
        // 내구도에 0.9를 곱한다.
        item.duration *= 0.9f; // 아이템의 내구도를 감소시키기 위해서
    }
}

 

위의 예시 코드를 보라. 한 눈에 훑어보기만 해도 알 수 있을것 같은 코드에 매 라인마다 주석을 달아둠으로써 오히려 읽기가 어려워졌다. 주석을 다는 습관은 좋은 것이지만, 불필요한 주석까지 다는 습관이라면 나쁜 것이다.

 

주석을 다는 경우는 최적화 작업이 진행되어서 로직을 한 눈에 읽는 것이 불가능해졌을 때, 어떤 방식으로 코드가 작동하는지를 설명하기 위해 주석을 추가하는 것과, 해당 지점에서 어떤 작업을 해야하는지 To do를 작성하는 경우, 해당 코드를 수정할 때 다른 작업자가 어떤 작업에 유의해야 하는지 등의 경고를 남기는데 사용하는 것으로 한정하는 것이 좋다.

 

그 이외의 경우에는 주석을 최대한 자제하고 코드를 읽는 것으로 프로그램을 이해할 수 있게 클래스, 변수, 함수 이름을 작성하고 코드의 흐름을 적절히 하는 것이 좋다.

 

이전 코드 남겨놓기

 

프로그래머라면 아마 대부분이 어떤 코드에 대해서 수정사항이 발생했을 때, 그 부분을 완전히 지워버리지 않고 주석 처리만 해놓고 새로운 코드를 작성한 경험이 있을 것이다.

 

public class Aim : MonoBehaviour
{
    [SerializeField]
    private Color aimColor;

    [SerializeField]
    private Image aimImage;
    private Camera mainCam;

    private Animator animator;

    private void Awake()
    {
        animator = GetComponent<Animator>();
    }

    public void AimingStart()
    {
        //StartCoroutine(ShowAim());
        animator.SetBool("isAiming", true);
    }

    //private IEnumerator ShowAim()
    //{
    //    float timer = 0f;
    //    while(timer <= 1)
    //    {
    //        timer += Time.deltaTime;
    //        var alpha = Mathf.Lerp(0f, 0.5f, timer);
    //        aimImage.color = new Color(aimColor.r, aimColor.g, aimColor.b, alpha);
    //        yield return null;
    //    }
    //}

    public void Aiming(Vector3 aimStartPos, Vector3 aimDirection, Vector2 aimHit)
    {
        if (mainCam == null)
        {
            mainCam = Camera.main;
        }
        var dist = Vector2.Distance(aimStartPos, aimHit);
        aimImage.pixelsPerUnitMultiplier = dist * 0.3f;
        aimImage.rectTransform.localScale = new Vector3(aimImage.rectTransform.localScale.x, dist);
        aimImage.rectTransform.position = mainCam.WorldToScreenPoint(aimStartPos);
        aimImage.rectTransform.up = aimDirection;
    }

    public void AimingEnd()
    {
        animator.SetBool("isAiming", false);

        //StopAllCoroutines();
        //StartCoroutine(HideAim());
    }

    //private IEnumerator HideAim()
    //{
    //    float timer = 0f;
    //    while (timer <= 1)
    //    {
    //        yield return null;
    //        timer += Time.deltaTime;
    //        var alpha = Mathf.Lerp(aimColor.a, 0f, timer);
    //        aimImage.color = new Color(aimColor.r, aimColor.g, aimColor.b, alpha);
    //    }
    //}
}

 

바로 이 코드처럼 말이다. 이처럼 띄엄띄엄 이전 버전의 코드를 남겨두게 되면 코드를 한 눈에 읽기 힘들어지고, 특히 여러 버전의 코드가 겹겹이 쌓이게 되면 나중에 복구하려고 하는 시점에는 어떤게 어떤 버전인지도 헷갈리게 된다. 만약 이전 버전으로 되돌아가야할 일이 있다면 버전이 바뀔 때마다 SVN같은 버전 관리 툴에 업데이트를 한 뒤, 차라리 롤백을 하라. 혼자서 하는 작업이라 SVN을 쓰기 귀찮은 상황이라도 차라리 귀찮음을 무릅쓰고 SVN을 쓰는 것이 최선이며 차선은 백업 폴더에 버전 별로 백업을 해두고 원본에서는 지난 코드를 지우는게 최선이다.

 

아스키아트

 

가독성을 해치는 것 외에도 쓸데없이 개발 효율을 낮추는 것들도 있다.

 

// ========================================= //
//  ||   /||   //||==\\ //==\\ ==== ||\      //
//  ||  //||  // ||  || ||      ||  ||\\     //
//  || // || //  ||==// ||  ==  ||  || \\    //
//  ||//  ||//   || \\  ||  ||  ||  ||==\\   //
//  ||/   ||/    ||  \\ \\==// ==== ||   \\  //
// ========================================= //

 

개발을 진행하다보면 프로그램을 개발하는 것보다 주석을 아름답게 꾸미는 것에 더 관심이 많아보이는 개발자들이 있다. 이러한 작업을 아스키아트라고 한다. 위 예시는 아주 간단한 아스키아트이다(더 멋진 아스키아트를 그릴 수 있었으나 웹페이지에 여백이 부족해 그리지는 않겠다). 아름답지 않은가? 지금이라도 당장 코드를 꾸미러 가고 싶지 않은가? 로망이란 멋있지만 쓸모없는 것을 가리킨다.

 

/* ------------------------------------------------------------------- *
 * Code Writer :: WERGIA                                               *
 * Last Modifier :: SOMETHING-WHO                                      *
 * Last Modified :: 2019/11/5                                          *
 * Version :: 1.1                                                      *
 * ------------------------------------------------------------------- */

 

코드 파일의 버전과 작성자 수정일자 등을 표시하는 것은 좋다. 나쁜 것은 끝 라인에 붙은 *들이다. 작성자는 완벽한 사각형을 만들었다고 좋아하겠지만 나중에 버전이 바뀌거나 수정자가 바뀌는 경우, 안의 내용을 수정해야 하는데 이 과정에서 마지막 끝 줄의 *은 엉망진창이 될 것은 필연적인 일이다. 이 라인을 맞추는 작업에 수정자가 쓸데없는 시간을 쏟느니 그냥 저 *들을 지워버리는게 낫다.

 


 

읽기 좋은 코드를 작성하기 위한 방법들은 여러 가지가 있지만 그것들이 지향하고자 하는 목표는 모두 같다. 코드의 가독성을 상승시키고 유지보수를 하기 쉽게 만드는 것이다. 개발자는 하나의 코드 스타일에 너무 매몰되지 않아야하고 팀의 협업 시스템에 맞춰 스타일을 변경할 수 있어야 한다. 그러면서도 실수를 최대한 줄일 수 있는 스타일을 유지해야만 한다.

 

  1. 팀 단위로 프로그래밍 작업할 때, 버전을 관리하기 위한 툴 [본문으로]

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

Programming 

static 키워드를 파일 경로와 URL 표현에 사용하기

 

작성 기준 버전 :: 2019.1.4f1

 

유니티 엔진으로 게임을 만들 때, 스크립트 작업은 대부분 C# 스크립트로 이루어진다. 한 때 유니티 초기에는 자바 스크립트(Java Script)나 부(Boo) 같은 언어도 지원을 했었지만, 최신 버전의 유니티 엔진은 C#만을 지원한다. 그렇기 때문에 C#에서 지원하는 기본적인 문법을 충분히 배우고 활용하는 법을 공부해야한다.

 

이번에는 유니티에서 static 키워드를 활용하여 파일 경로와 URL 표현에 사용하는 방법에 대해서 알아볼 것이다. C#의 static 키워드에 대한 기본적인 내용은 링크를 통해서 확인할 수 있다.

 

 

파일 경로와 URL 표현

 

public class FileLoader : MonoBehaviour

{

    void Start()

    {

        LoadSomeFile(Application.dataPath + "/Save/" + "fileName.txt");

    }

 

    public void LoadSomeFile(string filePath)

    {

        // 파일을 로드하는 작업...

        Debug.Log(filePath);

    }

}

 

public class UrlDownloader : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(DownloadSomeFile("https://SomeUrl/GameData/" + "fileName.png"));
    }

    public IEnumerator DownloadSomeFile(string filePath)
    {
        UnityWebRequest request = new UnityWebRequest(filePath);
        yield return request.SendWebRequest();
        var data = request.downloadHandler.data;
        // URL에서 받아온 데이터로 작업...
    }
}

 

모든 프로그래밍도 마찬가지겠지만 게임 프로그래밍 역시 게임 저장/불러오기나 네트워크 게임이라면 게임 데이터 받아오기 등의 파일 경로와 URL을 다루어야 할 일이 발생한다. 하지만 위의 코드처럼 경로와 URL을 코드에 하드코딩을 해버리면 나중에 파일 경로나 URL이 바뀌는 경우가 발생했을 때, 변경된 경로를 모두 찾아서 바꾸어야 하는 번거로움이 발생한다. 그리고 그 중에 하나라도 놓치는 경우가 발생한다면, 그것은 곧바로 게임이 제대로 동작하지 않은 버그로 직행한다.

 

이러한 문제를 막기 위해서 게임에서 사용되는 모든 경로는 하나의 클래스로 묶어두고 그 클래스에서 경로를 가져오도록 만드는게 좋다. 다만 클래스에서 경로를 불러올 때는 객체를 생성하지 않고 곧바로 불러올 수 있게 하는 것이 좋다. 바로 그런 점에서 static 키워드를 적용하면 매우 좋다.

 

public static class GamePath

{

    public static string savePath = Application.dataPath + "/Save/";

}

 

public static class GameURL

{

    public static string GameDataURL = "https://SomeUrl/GameData/";
}

}

 

위 예시 코드처럼 정적 클래스와 정적 변수를 만들어서 경로를 표현한다.

 

public class FileLoader : MonoBehaviour

{

    void Start()

    {

        LoadSomeFile(GamePath.savePath + "fileName.txt");

    }

 

    public void LoadSomeFile(string filePath)

    {

        // 파일을 로드하는 작업...

        Debug.Log(filePath);

    }

}

 

public class UrlDownloader : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(DownloadSomeFile(GameURL.GameDataURL + "fileName.png"));
    }

    public IEnumerator DownloadSomeFile(string filePath)
    {
        UnityWebRequest request = new UnityWebRequest(filePath);
        yield return request.SendWebRequest();
        var data = request.downloadHandler.data;
        // URL에서 받아온 데이터로 작업...
    }
}

 

경로를 사용할 때는 바로 위 예시 코드처럼 사용하면 된다. 그러면 만약에 경로가 변경되었을 때, 모든 코드에서 수정된 경로를 일일이 찾아서 바꿀 필요없이 GamePath 클래스와 GameURL 클래스의 경로만 수정하면 모든 코드에 적용이 된다.

 

이런 식으로 코드 내에 상수로 들어가지만, 추후에 변경이 발생할 수 있는 부분을 정적 클래스로 묶어서 관리하면 좋다.

 

[유니티 어필리에이트 프로그램]

아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.

 

에셋스토어

여러분의 작업에 필요한 베스트 에셋을 찾아보세요. 유니티 에셋스토어가 2D, 3D 모델, SDK, 템플릿, 툴 등 여러분의 콘텐츠 제작에 날개를 달아줄 다양한 에셋을 제공합니다.

assetstore.unity.com

 

Easy 2D, 3D, VR, & AR software for cross-platform development of games and mobile apps. - Unity Store

Have a 2D, 3D, VR, or AR project that needs cross-platform functionality? We can help. Take a look at the easy-to-use Unity Plus real-time dev platform!

store.unity.com

 

Create 2D & 3D Experiences With Unity's Game Engine | Unity Pro - Unity Store

Unity Pro software is a real-time 3D platform for teams who want to design cross-platform, 2D, 3D, VR, AR & mobile experiences with a full suite of advanced tools.

store.unity.com

[투네이션]

 

-

 

toon.at

[Patreon]

 

WER's GAME DEVELOP CHANNEL님이 Game making class videos 창작 중 | Patreon

WER's GAME DEVELOP CHANNEL의 후원자가 되어보세요. 아티스트와 크리에이터를 위한 세계 최대의 멤버십 플랫폼에서 멤버십 전용 콘텐츠와 체험을 즐길 수 있습니다.

www.patreon.com

[디스코드 채널]

 

Join the 베르의 게임 개발 채널 Discord Server!

Check out the 베르의 게임 개발 채널 community on Discord - hang out with 399 other members and enjoy free voice and text chat.

discord.com

 

반응형

+ Recent posts