본문 바로가기

Game/Unity & C#

Unity Clean Code

Unity Clean Code

(원본 link : https://github.com/sampaiodias/unity-clean-code#methods)

The Basics

  • 이 가이드를 읽고 있다면 아마도 C#이 무엇인지, Unity에서 간단한 스크립트를 작성하는 방법과 그렇지 않은 것에 대한 일반적인 이해가 있을 것이다. 그러나 프로그래머가 새 스크립트를 만들 때 가장 먼저 보게되는 원칙을 이해하려고 애쓰는 경우가 종종 있다. 다음을 보자:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class MyCustomScript : MonoBehaviour {
    
        // Use this for initialization
        void Start () {
    
        }
    
        // Update is called once per frame
        void Update () {
    
        }
    }

    위의 코드는 Unity에서 새로운 C# 스크립트의 기본 템플릿이다. 우리는 몇 부분으로 나눌 수 있다.

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

    스크립트의 첫 줄은 코드를 작동시키는 데 필요한 코드 라이브러리를 "importing" 하는 전용이다. 의존성이라고 생각해라: UnityEngine 라이브러리의 기능을 명시적으로 선언하지 않으면 사용할 수 없다. 스크립트의 첫 줄에 그대로 두어라.

    public class MyCustomScript 

    우리가 만든 스크립트 파일은 클래스(객체의 추상화)를 정의한다. 이 클래스는 공용(다른 코드 조각으로 액세스 가능)과 입력한 파일 이름으로 정의된다. 이 경우 클래스를 MyCustomScript라고 한다. 이것은 끔찍한 이름이다.
    이 클래스의 이름(목표가 되어야 함)만 보고 이 클래스의 기능을 이해할 수 있는 방법이 없다. Pascal Case에서는 항상 "PlayerHealth" 또는 "Projectile"과 같은 의미있는 이름을 사용해라.

                                : MonoBehaviour {
    
        // Use this for initialization
        void Start () {
    
        }
    
        // Update is called once per frame
        void Update () {
    
        }
    }

    맨 위(공개 클래스 MyCustomScript 이후)부터 시작해 자체 클래스에서 상속 할 클래스(있는 경우)를 정의한다. 이 경우 Unity의 MonoBehaviour이다. 이것은 Unity를 위한 게임 개발에서 가장 중요한 클래스일 것이다.
    클래스는 GameObject에 부착 할 수 있는 컴포넌트로 클래스를 "표시"하기 때문이다.
    다른 클래스에서 상속하는 것은 단순히 클래스를 표시하는 것이 아니라 "다른 클래스에 정의된 동작을 재사용, 확장 및 수정하는 새 클래스를 만들 수 있게 하는 것"이다. 예를 들어, 시작 및 업데이트 방법은 클래스가 MonoBehaviour 인 경우 Unity가 미리 정의된 이벤트 중에만 호출하는 것으로 적절한 기능으로 필요한 메소드를 확장한다.

    초보자들이 흔히 저지르는 실수 중 하나는 모든 것이 MonoBehaviour이거나 다른 것에서 상속해야 한다고 생각하는 것이다. 그것은 사실과는 거리가 멀고 실제로는 나쁜 습관이다. 예를 들어, 데이터만 처리하는 개념 또는 추상화가 있는 경우 다른 클래스에서 상속하지 않는 클래스로 만드는 것이 좋다.

Identation

  • 언뜻 보기에 코드 내 공간 관리는 간단한 주제이다. 공백, 탭, 줄 바꿈으로 다른 단어나 기호를 분리해 코드를 더 읽기 쉽게 만든다. 올바른 ID를 가진 코드의 중요성을 이해하는 것은 종종 프로그래밍에 익숙하지 않은 개발자에게는 어려우며, 내가 찾은 가장 좋은 방법은 실제로 잘못된 ID를 가진 코드를 보여주는 것이다. 스크립트 템플릿을 새로운 수준으로 가져가보자:

    // The code below is an example of *bad* identation.
    
    using System.Collections; using System.Collections.Generic;
    using UnityEngine;
    public class MyCustomScript    :MonoBehaviour{
    //Use this for initialization
    void Start() {
    }
    // Update is called once per frame
        void Update ( ) { } 
    }

    위의 코드를 읽는 것이 얼마나 어려운가? Unity 프로그래밍에 익숙하지 않다면 스크립트의 기능과 각 부분의 위치를 이해하는 것이 이미 시간이 많이 걸린다.

    많은 프로그래밍 언어는 코드를 올바르게 식별하는 방법에 대한 지침이나 규칙을 제안했지만, 실제로 배우는 것은 대부분의 사람들에게 쉬운 일이 아니다. 운 좋게도 이 프로세스를 쉽게 수행 할 수 있는 도구인 "자동 서식"이 있다. 자신이 직접 식별을 처리 할 수 있는지 확실하지 않은 경우 컴퓨터에서 작업을 수행하고 예를 들어 학습하도록 하자.

    Visual Studio를 사용해 코딩하는 경우 Visual Studio 용 'Productivity Power Tools' 플러그인을 설치하고, "저장시 문서 형식" 옵션을 활성화하는 것이 좋다. 설치하지 않으려면 항상 "Ctrl + K, Ctrl + D" 바로가기를 사용해 현재 열려있는 스크립트를 포맷해라.

Variables

  • 아시다시피, 변수(또는 기술적으로 말하면 멤버)는 런타임 동안 응용 프로그램의 데이터를 저장하는 것이다. 게임에는 많은 양의 데이터가 있을 수 있으므로 의미 있고 읽기 쉬운 이름을 갖는 것이 중요하다. 여기서는 변수 직렬화에 대해 이야기 할 것이다. Unity에서 특히 중요한 주제이다.

Naming

  • 이것은 커뮤니티 내에서 까다로운 주제이다. 특정 언어에는 매우 엄격한 명명 규칙이 있지만 C#은 몇 가지 경우에 약간 "완화"되어 있어 조직이 결정할 수 있는 규칙을 열어둔다. 이를 염두에 두고, 아래에서 보게 될 것은 변수 이름 지정을 위한 C# 지침(필드 및 속성 포함)과 매우 일반적이고 널리 채택 된 것의 혼합이다.

  • User meanigful names

    // Do
    public int healthAmount;
    public string teamName;
    // Do NOT
    public int hp;
    public string tName;
  • Use readable names

    // DO
    public int movementSpeed;
    // DO NOT
    public int mvmtSpeed;
  • Use nonuns as names for variables

    // DO
    public int movementSpeed;
    // DO NOT
    public int getMovementSpeed;
  • Use the correct "casting" for the kind of variable

    public int movementSpeed;            // Public variable, Camel Case
    private int _movementSpeed;        // Private variable, Camel Case with optional '_' at Start
    public int MovementSpeed {get; set; } // Property, Pascal Case
    private const int MovementSpeed = 10; // Constant, Pascal Case
  • Avoid using abbreviations or single characters (unless it's math-related)

    // Do
    public string groupName;
    // Do NOT
    public int grpName;
    
    // Recommended for math-related scripts, like Vector2
    public int x;
    public int y;
  • Explicitly use the 'private' keyword

    // Do
    private bool _isJumping;
    // Do NOT
    bool _isJumping;
  • Use the 'var' keyword when the second part of the variable attribution clearly reveals the type. Only for variables declared inside a method or scope (local variable)

    // Do
    var players = new List<Players>();
    // Do NOT
    var players = PlayerManager.GetPlayers();

Serialization

  • Unity를 사용해 C# 스크립트를 생성하고 클래스가 MonoBehaviour에서 상속되도록 할 때 인스펙터 창을 사용해 엔진에 "공용"인 값을 편집 할 수 있다. 이 작업을 수행하고 장면을 저장 한 후 이 값은 장면 파일에 직렬화(또는 저장)된다. 직렬화 된 변수를 사용하거나 사용하지 않을 때는 말하는 것이 목적이 아니므로 직렬화 할 변수를 선언하는 방법에 중심을 두자:

    // Serialized
    public int movementSpeed;
    public int movementSpeed = 10;
    [SerializeField] private int _movementSpeed;
    [SerializeField] private int _movementSpeed = 10;
  • 참고로 [SerializeField]와 같은 속성을 다음과 같이 가독성을 높이기 위해 선언 위의 줄에 배치 할 수 있다:

    [SerializeFiel]
    private int _movementSpeed;
    [SerializeField, AnotherCoolAttribute]
    private bool _isEnemy;

Methods

  • 특성상 메소드를 작성할 때는 항상 동사를 사용해 이름을 지정해야 한다. 변수 이름 지정에 대해 위에서 배운 것을 잊지 말아라. 설명적이고 의미 있고 읽기 쉬운 이름을 사용해라. 또한, Pascal Case를 사용해 메소드 이름을 지정해라.

    // Do
    public void SetInitialScore()
    {
    
    }
    
    // Do NOT
    public void InitialScore()
    {
    
    }
    
    public void setInitialScore()
    {
    
    }
    
    public void SetInitScr()
    {
    
    }
  • 메소드의 주요 기능은 매개 변수이다. 그것들을 만들 때 Camel Case를 사용하고 접두사를 피해라.

    // Do
    public bool IsNewHighScore(int currentScore)
    {
    
    }
    
    // Do NOT
    public bool IsNewHighScore(int CurrentScore)
    {
    
    }
    
    public bool IsNewHighScore(int _currentScore)
    {
    
    }
  • 적은 양의 정보로 분석법의 구조를 명확하게 유지해라. 메소드에 10줄 이상의 코드가 포함된 경우 두 개 이상의 메소드로 분할하는 것이 좋다. 마지막으로 식별 주제로 돌아가서 아래 구조를 따라 메소드를 찾아라:

    public void DoSomething()
    {// 메소드 선언의 동일한 위치에서 시작하여 새 행에서 중괄호
        // 메소드 내용은 중괄호 바로 뒤에 '탭'이다.
      string somethingCool = "Cool";
      DoSomethingElse(somethingCool);
    }

Statements

  • Microsoft가 정의한 명령문은 변수 선언, 메서드 호출 및 컬렉션 반복과 같은 "프로그램이 수행하는 작업"이다. 문장을 한 줄과 여러 줄로 나눌 수 있다.

    private void DoSomething()
    {
        // Single-line statement. In this case, declaring and initializing a variable;
      var randomNumbers = new int[3];
    
      // Multi-line statement. In this case, looping through 'randomNumbers'
      foreach (int number in randomNumbers)
      {
           DoSomethingElse(number);
      }
    }
  • 한 줄짜리 문장의 경우 실제로 모든 문장을 별도의 줄에 유지하는 것, 너무 길지 않은 코드 줄 및 기타 작은 팁을 제외하고 실제로 이야기 할 수 있는 것은 많지 않다. 그러나 한 가지 예외가 있다: 줄 바꿈. 긴 상태문을 나누는 것이 어색하거나 번거로울 때 사용해라.

    // Do
    Debug.Log("This is just an example log that will print a long text with the values of the script: " + PlayerName + " " + playerHealth);
    
    string logMessageForPlayersInformation =
                  PlayerManager.GetPlayerInformation(GetPlayerIndex("Player1")) +
                  PlayerManager.GetPlayerInformation(GetPlayerIndex("Player2"));
    Debug.Log(logMessageForPlayersInformation);
    
    // Do Not
    
    /// Too long
    Debug.Log("This is just an example long that will print a long text with the values of the script: " + playerName + " " + playerHealth)
    
    string logMessageForPlayersInformation =
                  PlayerManager.GetPlayerInformation(
                                    GetPlayerIndex("Player1")) +
                  PlayerManager.GetPlayerInformation(
                                    GetPlayerIndex("Player2"));
    Debug.Log(logMessageForPlayersInformation);

    복잡성은 실제로 여러 줄로 된 문장에 관한 것이다. 다른 블록 안에 코드 블록을 만드려고 하면 스크립트가 위에 제시된 문제와 비슷한 문제에 직면하게 된다. 이 예제를 살펴보자:

    var someValue = 100;
    var myValues = new int[10, 5]
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            if (i >= 1)
            {
                someValue--;
            }
            else
            {
                while (someValue > 50)
                {
                    someValue -= 10;
                }
            }
            myValues[i, j] = someValue;
        }
    }

    위의 코드는 특히 유용한 것을 수행하기 위한 것이 아니지만 이 알고리즘의 기능을 이해하는 작업을 받았다면 어려운 일이다. 이러한 중첩된 명령문으로 인해 코드에 추가되는 복잡성의 정도는 가독성과 유지관리성이 매우 낮다. 그리고 실제 게임에 실제로 제공된 코드 조각의 더 나쁜 예를 보았다. 일반적으로 다른 여러 줄 문 안에 최대 하나의 여러 줄 문을 사용해 메소드를 유지해라. 가능하면 중첩 또는 다중행 명령문은 전혀 바람직하지 않다.

    // Goal
    var someValue = 100;
    var myValues = new int[10, 5]
    for (int i = 0; i < 10; i++)
    {
        for (int j = 0; j < 5; j++)
        {
            DoSomething(someValue, i, j);
        }
    }
    
    // Ultimate Goal
    int[,] myValues = CalculateValueMatrix(100, 10, 5);

    이상한 모양의 코드를 생성 할 수 있는 여러 줄 문장의 흥미로운 기능이 있다. 여러 줄 명령문이 해당 범위 내에 하나의 명령문만 포함하는 경우 중괄호를 제거하면 컴파일러에서 오류가 발생하지 않는다. 이 작업을 수행하면 코드가 훨씬 깨끗해지지만 때로는 반대가 되는 경우가 있다. 아래는 이것에 접근하는 방법에 대한 몇 가지 예이지만, 무엇을 하든지 코드 전체에서 이 선택을 일관되게 해라.

    // Do
    
    for (int i = 0; i < 10; i++)
        DoSomething();
    
    if (_isJumping == false) return;
    
    // Avoid
    
    if (_isJumping == false) DoSomething();
    
    // Do NOT
    
    // Nesting multi-line statements without braces
    for (int i = 0; i < 10; i++)
        for (int j = 0; j < 10; j++)
            DoSomething();

Namespaces

  • C#의 가장 유용한 기능 중 하나인 네임 스페이스는 스크립트를 보다 잘 구성할 수 있는 방법이다. 불행히도 네임 스페이스는 Unity 개발자가 오랫동안사용하지 않은 C# 기능 중 하나였다. (아마도 Unity 자체의 스크립팅 문서가 제대로 표시되지 않았기 때문일 수 있음) 하지만 개인적으로 커뮤니티 내에서 일부 변경 사항이 있다. 네임 스페이스는 유용할 뿐만 아니라 강력하지만, 제대로 사용할 때만 유용하다. 그리고 가장 중요한 부분은 네이밍이다.

  • 네임스페이스로 해야 할 일과 하지 말아야 할 일로 가기 전에 이 친숙한 코드를 먼저 보자:

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

    모든 새로운 MonoBehaviour는 UnityEngine, System.Collections 및 System.Collections.Generic 네임 스페이스를 사용한다. UnityEngine 네임 스페이스는 Unity에서 제공하며 MonoBehaviour 클래스와 같은 많은 스크립트를 포함한다. 다른 것들은 Microsoft에서 제공하며 목록 및 사전과 같은 다양한 개체 모음을 정의하는 많은 클래스와 추상화도 포함한다. 네임 스페이스의 중요한 부분 중 하나는 다른 네임 스페이스 내에 네임 스페이스를 만들 수 있다는 것이다. 예를 들어 컬렉션은 실제로 시스템 네임 스페이스 안에 중첩된다.

  • 이제 이러한 개념이 배후에 있으므로 개념을 작성하고 이름을 지정하는 방법을 살펴보겠다!

    using System;
    
    namespace Company.Product.Feature
    {
        public class Example
        {
    
        }
    }

    이 파일이 사용하는 다른 네임스페이스를 선언 한 후 네임스페이스를 만들고 이 네임스페이스 범위 내에서 다른 모든 코드를 캡슐화한다. 이름은 조직 이름으로 시작한 다음 제품 이름으로 시작해야한다. (예: Github.ExampleProduct). 모든 이름은 파스칼 케이스에 있으며, 중첩된 계층을 분리하는 점이 있다. 이제 제품의 특정 기능 이름(예: Github.ExampleProduct.Database)을 사용해 중첩을 계속하거나 사용하지 않을 수 있다. 모든 이름은 단수여야 하지만 네임 스페이스 이름에 포함된 내용(예: Collections, System.Collections)을 더 잘 설명할 수 있도록 하려면 복수형을 사용하는 것이 좋다. 마지막으로 밑줄과 같은 접두사나 다른 기호를 사용하지 말아라.

Comments

  • 인간이 코드를 이해하도록 돕는 명예로운 목적으로 주석은 스크립트를 문서화하고, 유지-관리성을 향상시키는 놀라운 방법이다. 그것들은 사용하기 쉽지만 남용하기도 쉽다. 코드의 품질을 높이려면 주석을 작성해야 할 때 간단한 지침이 있다.

    1. 클래스, 메소드, 열거형 또는 구조체를 문서화한다.
    2. 파일의 헤더 역할을 한다. (주로 저작권 표시)
    3. 본질적으로 복잡하거나 이해하기 어려운 진술을 설명한다.
  • 첫 번째 상황부터 시작해 이 세 가지 상황을 각각 살펴보자. 위에서 언급한 구조 중 하나의 위 줄에 /(대시 기호)를 세 번 입력하면 특수 주석 섹션이 나타난다.

    /// <summary>
    /// 
    /// </summary>
    public class Example
    {
    
    }

    이것은 코멘트를 예쁘거나 읽기 쉽게 만드는 규칙이 아니다. 이것은 실제로 가장 익숙한 IntelliSense와 같은 자동화된 도구가 주석을 구문 분석하고, 유용한 컨텐츠를 생성하는데 도움이 되는 표준이다. 이 때문에 이 패턴을 사용해 코드를 문서화해라.

    // Do
    
    /// <summary>
    /// Calculates the total area of a circle.
    /// </summary>
    /// <param name="radius">The radius of the circle</param>
    private float CalculateCircleArea(float radius)
    {
        return 3.14f * radius * radius;
    }
    
    //Do NOT
    
    // Calculates the total area of a circle, given a certain radius.
    private float CalculateCircleArea(float radius)
    {
        return 3.14f * radius * radius;
    }

    두 번째 상황에서는 표준이 많지 않기 때문에 문서의 첫 줄에서 시작하는 한 원하는 방식으로 자유롭게 입력 할 수 있다. 예를 들면 다음과 같다:

    // --------------------------------------------------------------------------------------------------------------------
    // <copyright file="Script.cs" company="{Company}">
    //
    // Copyright (C) 2019 {Company}
    //
    // This program is free software: you can redistribute it and/or modify
    // it under the +terms of the GNU General Public License as published by
    // the Free Software Foundation, either version 3 of the License, or
    // (at your option) any later version.
    //
    // This program is distributed in the hope that it will be useful,
    // but WITHOUT ANY WARRANTY; without even the implied warranty of
    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    // GNU General Public License for more details.
    //
    // You should have received a copy of the GNU General Public License
    // along with this program.  If not, see http://www.gnu.org/licenses/. 
    // </copyright>
    // <summary>
    // {one line to give the program's name and a brief idea of what it does.}
    // 
    // Email: {Email}
    // </summary>
    // --------------------------------------------------------------------------------------------------------------------
    
    // Rest of the code goes here

    마지막 상황은 경험이 없는 프로그래머가 주석을 "남용"해 많은 수의 진술을 하는 것이다. 이것은 불필요하며 실제로는 좋은 점보다 안 좋은 점이 더 많다. 의견을 작성하는 것은 표준이 아닌 특별한 경우를 위한 것이어야 한다. 정확히 언제 사용해야하는지 말하기는 어렵기 때문에 아래 예제(주석이 완전히 쓸모없는 곳)를 따르므로 동일한 실수가 발생하지 않는다.

    // Do NOT! Once again, this is an example of what you should NOT do!
    
    private void CreateEnemy(GameObject prefab)
    {
        // The GameObject of the enemy to be instantiated
        GameObject enemy;
        // Creates the enemy in the scene
        enemy = Instantiate(prefab)
    
        enemy.GetComponent<Enemy>().InitiateMovementBehaviour(); // Makes the enemy walk around the world
    }