Pyinstaller는 파이썬 코드를 독립 실행형 실행 파일로 변환하기 위한 라이브러리입니다.

프로그램을 제작하면서 사용되는 다양한 이미지나 파일을 포함하는 방법을 알아보겠습니다.

 

우선 --ondir 모드에서 파일을 추가하는 방법을 알아보겠습니다.

 

--onedir 모드에서 파일을 추가하는 것은 비교적 간단합니다. 이미지 파일을 py 스크립트와 같은 경로에 배치하면 Pyinstaller가 자동으로 출력 경로에 포함시킵니다.

 

testApp
|-main.py
|-image.png

 

이렇게 포함된 파일은 --add-data 옵션을 사용하여 경로 상의 파일을 빌드에 포함시킬 수 있습니다. 루트 경로의 다른 폴더에 있는 파일을 포함시키고 싶은 경우 아래와 같이 포함하면 됩니다.

 

pyinstaller --onedir --add-data="image.png;." main.py

 

문자열 끝의 '.'은 루트 폴더에 위치시키겠다는 뜻입니다. 또한 ';'는 소스 및 대상 경로 뒤에 있는 구분 기호입니다.

리눅스의 경우 ';'(세미콜론) 대신 ':'(콜론)을 사용해야 할 수도 있습니다.

 

pyinstaller --onedir --add-data "images/image.png;images/" main.py

 

이 경우 Pyinstaller에게 images 서브 경로에 위치한 image.png 파일을 포함시켜 출력 경로의 images라는 이름의 서브 경로에 배치하라는 의미입니다.

 

 

다음으로는 --onefile 모드에서 파일을 추가하는 방법을 알아보겠습니다.

 

우선 onefile 모드에 대해서 간단히 알아보겠습니다. 이미지 파일을 포함한 모든 파일을 하나의 실행 파일로 묶는 모드를 말하는데요, 실행 파일이 실행되면 번들 파일을 임시 경로에 추출하고 거기서 응용프로그램이 실행되는 구조입니다.

 

이 언패킹 과정에서 시간이 걸리기 때문에 onedir 모드에 비해서 다소 느릴 수도 있습니다.

 

프로그램 어딘가에서 "banner.png" 파일이 사용되며 이미지가 파이썬 스크립트와 같은 경로에 위치한다고 가정하겠습니다. 이후, 아래 코드와 같이 작성하고 프로그램을 실행하면 이미지가 출력된 것을 확인할 수 있습니다.

 

banner = tkinter.PhotoImage(file = "banner.png")

bannerPanel = tkinter.Label(window, image = banner)
bannerPanel.pack()

 

이후 pyinstaller를 통해 실행 파일을 생성해보겠습니다.

 

pyinstaller -w -F --onefile --add-data="banner.png;." main.py

 

빌드 후 프로그램을 실행하면 아래와 같이 banner.png 파일을 찾을 수 없다고 합니다.

 

 

 

이 때, 아래의 함수를 추가하여 경로를 가져오면 정상적으로 이미지가 출력되는 것을 확인할 수 있습니다.

 

def resource_path(relative_path):
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)
    
banner = tkinter.PhotoImage(file = resource_path("banner.png"))

bannerPanel = tkinter.Label(window, image = banner)
bannerPanel.pack()

 

 

참조 : https://coderslegacy.com/add-image-data-files-in-pyinstaller-exe/#google_vignette

블로그 이미지

NIA1995

,

1. Mass : 질량 값으로 중력에 영향을 받지 않고 Rigidbody 컴포넌트끼리 충동했을 때 영향을 결정함.

2. Drag : 공기 저항값, 0에 가까울수록 무거워 보이고 크면 가벼워 보임.

3. Angular Drag : 회전을 할 때의 공기 저항 값

4. Is Kinemetic : 물리 엔진이 아닌 게임 오브젝트의 로직에 따라 이동할 것인가를 결정

5. Interpolate : 물리 엔진에서의 애니메이션의 자연스러운 보간 여부

6. Collision Detection : 충돌 처리 조건 여부( 조건, 연속 등 등)

7. Freeze Position, Rotation : 해당 옵션을 킬 경우 물리 엔진에서 처리 되는 값들을 사용하지 않고 고정 시킬 수 있음.

 

블로그 이미지

NIA1995

,

인벤토리 관련 해외 에셋을 둘러보던 중 IsDirty라는 bool값의 뜻이 궁금해 찾아봤습니다.

 

프로그램에서 객체가 로직을 수행 중 해당 객체의 값이 변경되었다는 것을 체크하기 위해 사용하는 flag라고 합니다.

 

참고 자료

https://jhedde.tistory.com/entry/Dirty-flag

블로그 이미지

NIA1995

,

게임을 제작하면서 데이터를 저장은 필수불가결입니다. 특히 RPG의 경우라면 많은 아이템이 필요할 겁니다. 이때 스크립트 가능한 오브젝트를 하나의 방법으로 사용할 수 있습니다. 스크립트 가능한 오브젝트는 대량의 데이터를 저장하기 위해 사용되는 데이터 컨테이너입니다.

 

일반적으로 게임 내에 플레이하며 사용되는 데이터들은 일반적으로 클래스로 구현하게 됩니다. 예를 들어 대량의 몬스터 스폰(뱀파이어 서바이벌)이나 대량의 아이템의 드롭(RPG 게임의 재료 아이템) 등의 경우 복사된 사본만큼 메모리를 소모하게 됩니다. 하지만 스크립트 가능한 오브젝트를 통해 구현을 하게 된 경우 메모리에 스크립트 오브젝트의 원본 데이터를 저장하고 이 데이터를 참조하여 메모리 소모를 줄일 수 있습니다.

 

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

[CreateAssetMenu(fileName = "New Card", menuName = "Scriptable Object/Card", order = 0)]
public class Card : ScriptableObject 
{
    public new string name;
    public string description;

    public Sprite artwork;

    public int manaCost;
    public int attack;
    public int health;

    public void Print()
    {
        Debug.Log(name + " : " + description + " The card Costs : " + manaCost);
    }
}

 

스크립트 가능한 오브젝트를 사용하는 방법은 간단합니다. 위의 예제와 같이 Monobehavior 대신 ScriptableObject 클래스를 상속받고 해당 클래스를 에셋의 형태로 제작할 수 있게 만들어주는 CreateAssetMenu 속성을 추가하고 원하는 데이터를 추가해주면 됩니다.

 

이때 주의할 점은 스크립트 오브젝트는 하나의 에셋 데이터를 서로 참조하는 형태이기 때문에 현재 체력이나 마나 같은 변할 수 있는 값을 사용해서는 안됩니다. 또한 에디터 상에서는 스크립트 오브젝트의 생성과 데이터 저장이 자유롭지만 배포된 빌드에서는 새로 생성이 불가능하고 기존의 만들어둔 에셋만 사용이 가능합니다. 마지막으로 스크립트 오브젝트는 게임 빌드에 포함되기 때문에 사용자에 의해 악의적으로 데이터 변조가 가능할 수도 있기 때문에 멀티 플레이 게임에서는 사용에 주의가 필요합니다.

 

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

public class CardDisplay : MonoBehaviour
{
    public Card card;

    public Text nameText;
    public Text descriptionText;

    public Image artworkImage;

    public Text manaText;
    public Text attackText;
    public Text healthText;

    private void Start()
    {
        nameText.text = card.name;
        descriptionText.text = card.description;

        artworkImage.sprite = card.artwork;

        manaText.text = card.manaCost.ToString();
        attackText.text = card.attack.ToString();
        healthText.text = card.health.ToString();
    }
}

 

 

참고 주소

https://youtu.be/aPXvoWVabPY

https://youtu.be/7Qt4QNhM4nY

블로그 이미지

NIA1995

,

객체 지향 프로그래밍이란?

객체를 기반으로 하는 프로그래밍 ( OOP, Object Oriented Programming )

1. 프로그램을 여러 개의 독립된 객체 단위로 분할하여 각 객체들이 메시지를 주고 받고, 데이터를 처리하게 한다.
2. 객체 단위로 개발하기 때문에 개발에 유연성이 생기며 주로 유지 / 보수가 많고, 대규모의 소프트웨어 개발에 자주 사용된다.


객체란?

세상의 모든 것을 지칭 ( 사람, 자동차 등 )
추상적인 개념으로 데이터를 나타내는 속성과 행위를 나타내는 기능으로 구분하여 각 객체의 특징을 뽑아서 사용한다.

예) 자동차

속성 : 바퀴( 개수, 크기 등 ), 핸들( 반지름, 색상 등 ), 기어, 브레이크 등
기능 : 전진, 후진, 방향 전환, 감속 등


클래스란?

객체를 표현하는 속성(변수)와 기능(메소드)을 하나의 집합으로 묶은 단위

예) 붕어빵을 만들 때

붕어빵 틀에 반죽과 팥소를 넣고 돌리며 굽는다.

문어빵 틀 = 클래스
문어빵 = 객체 ( 객체를 클래스를 통하여 메모리를 사용해 실체화 할 경우 이것을 '객체의 인스턴스화' 라고 하며, 메모리를 가진 객체는 인스턴스이다. )

 

예)
Bread Baguette, Donut; => 브레드 클래스의 '객체'

Baguette = new Bread();
Dount = new Bread();
=> 바게트와 도넛은 브레드 클래스의 '인스턴스' ( 객체에 메모리를 할당 )

 

참고 자료

1. https://youtu.be/wbzF4BNw5Zs

2. https://gmlwjd9405.github.io/2018/09/17/class-object-instance.html

블로그 이미지

NIA1995

,

String 데이터 타입은 immutable Type인데 코드 상에서는 대입 연산자가 오버 로딩되어 있어 마치 mutable Type처럼 사용이 가능합니다. 하지만 내부적으론 새로운 공간에 새로운 값을 할당하여 오버헤드가 발생을 하게 됩니다.

 

이때 버퍼를 가지고 있는 StringBuilder 클래스를 사용하면 mutable Type으로 사용이 가능합니다. 따라서 String과 StringBuilder의 대입 연산을 반복적으로 돌려서 서로의 오버헤드를 알아보고자 합니다. 

 

using System.Text;
using System.Diagnostics;

private void OverheadTest()
{
	string test1 = "";

    Stopwatch time1 = new Stopwatch();
    time1.Start();

   	for (int i = 0; i < 100000; i++)
   	{
   	    test1 += i.ToString();
   	}

   	time1.Stop();

   	UnityEngine.Debug.Log("TEST 1 Time : " + time1.ElapsedMilliseconds);

   	StringBuilder test2 = new StringBuilder();

   	Stopwatch time2 = new Stopwatch();
   	time2.Start();

   	for (int i = 0; i < 100000; i++)
   	{
   	    test2.Append(i.ToString());
   	}

   	time2.Stop();

   	UnityEngine.Debug.Log("TEST 2 Time : " + time2.ElapsedMilliseconds);
}

실행 결과 --------
TEST 1 Time : 59987
TEST 2 Time : 74

 

각 각 10만번의 반복문을 돌린 결과 생각보다 큰 차이가 있었습니다. 따라서 문자열의 할당과 관련된 작업을 지속적이고 대량으로 할 경우 StringBuilder를 사용하면 될 것 같습니다.

 

블로그 이미지

NIA1995

,

정수형 변수
short : 16bit 부호가 있는 정수
int : 32bit 부호가 있는 정수
long : 64bit 부호가 있는 정수, L 접미어를 붙여줘야 한다.
 
ushort : 16bit 부호가 없는 정수, 0과 양수만 가능
uint : 32bit 부호가 없는 정수, 0과 양수만 가능, U 접미어를 붙여줘야 한다.
ulong : 64bit 부호가 없는 정수, 0과 양수만 가능, UL 접미어를 붙여줘야 한다.


실수형 변수
float : 32bit 실수, f 접미어를 붙여줘야 한다.
double : 64bit 실수, d 접미어 거를 수 있다.
decimal : 128bit 실수,  m 접미어를 붙여줘야 한다.

 

 

그 외
char : 16bit UniCode 문자
byte : 8bit, 1바이트 표현 단위, 예) 0x46

↑ Value 형식의 데이터 타입

string : 문자열
object : C#의 최상위 개체

↑ Reference 형식의 데이터 타입으로 Null 체크 가능

Value 형식의 경우 기본적으로 Null 체크가 불가능하지만, Nullable 변수를 사용하면 체크가 가능하다.

예) int? ix = null;

if(ix == null)
{
...
}  

블로그 이미지

NIA1995

,
간단한 시연 영상

https://youtu.be/9xz_kYJZ_zw 디아블로 형식의 인벤토리로 해당 유튜브 강좌를 보고 만들었습니다. 강좌에서는 (0, 0) 기준점인 첫 슬롯에서만 아이템 추가가 가능했는데 여기서 간단하게 모든 슬롯을 검사하여 아이템을 추가할 수 있도록 변경하고 마무리하였습니다.

 

코드는 전체적으로 어려움 없이 해석할 수 있었습니다. 기존에는 인벤토리에서 아이템을 제작할 때는 에디터 상에서 일일이 UI를 제작해서 사용했는데 해당 강좌에서는 코드로 UI를 다루는 법에 대해서 많이 배웠습니다. 

 

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

[System.Serializable]
public abstract class BaseItem
{
    public bool isSearched;

    public Texture2D image;

    /* 아이템이 차지하는 슬롯의 시작 위치 */
    public int x;
    public int y;

    /* 아이템이 차지하는 슬롯의 크기 */
    public int width;
    public int height;

    public abstract void PerformAction();
}

Base Item

 

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

[System.Serializable]
public class Item : BaseItem
{
    public override void PerformAction()
    {
        
    }
}

Item

 

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

[System.Serializable]
public class Slot
{
    public bool occupied; /* 슬롯 사용중 여부 */

    public Rect rectPosition;

    public Slot(Rect rectPosition)
    {
        this.rectPosition = rectPosition;
    }
}

Slot

 

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


public class Inventory : MonoBehaviour
{

    private static Inventory inventoryInstance = null;
    public static Inventory InventoryInstance()
    {
        if(inventoryInstance == null)
        {
            Debug.LogError("오브젝트가 존재하지 않습니다.");
        }

        return inventoryInstance;
    }

    /* 인벤토리 이미지 */
    public Texture2D image; /* 321 x 434 px */

    /* 인벤토리 Rect 값 */
    public Rect rectPosition;

    public List<Item> items = new List<Item>();

    /* 인벤토리 슬롯의 총 크기 10 * 4 */
    public int slotWidthSize = 10;
    public int slotHeightSize = 4;

    public Slot[,] slots;

    /* slotX, slotY : 인벤토리 전체 이미지(0, 0)을 기준으로 첫 슬롯(0, 0)의 까지의 거리 */ 
    public int slotX;
    public int slotY;

    /* 슬롯의 너비와 높이 */
    public int width = 29;
    public int height = 29;

    /* 마우스의 눌리고 떼졌을 때의 Vector2 값 */
    private Vector2 mouseDown;
    private Vector2 mouseUp;

    /* 아이템의 이동을 위한 간이 아이템 */
    private Item tempItem;

    private void Awake() 
    {
        inventoryInstance = this;
    }

    private void Start() 
    {
        SetSlots();    
    }

    /* 슬롯 초기화 */
    private void SetSlots()
    {
        slots = new Slot[slotWidthSize, slotHeightSize];;

        for(int x = 0; x < slotWidthSize; ++x)
        {
            for(int y = 0; y < slotHeightSize; ++y)
            {
                slots[x, y] = new Slot(new Rect((slotX + width * x), (slotY + height * y), width, height));
            }
        }
    }
    
    private void OnGUI() 
    {
        DrawInventory();
        DrawItems();
        DetectMouseOnGUI();
        DrawTempItem();
    }

    /* 인벤토리 이미지 출력 */
    private void DrawInventory()
    {
        rectPosition.x = Screen.width - rectPosition.width;
        rectPosition.y = Screen.height - rectPosition.height - Screen.height * 0.2f;

        GUI.DrawTexture(rectPosition, image);
    }

    /* 전체 아이템 이미지 출력 */  
    private void DrawItems()
    {
        for(int i = 0; i < items.Count; ++i)
        { 
            GUI.DrawTexture(
                            /* slotX, Y : 인벤토리 이미지(0,0)을 기준으로 첫 슬롯(0, 0)까지의 거리, 
                            rectPosition.X, Y: 해상도 Width - rectPosition Width, 
                            items[i].x, y : 그려질 아이템의 시작 위치
                            width, height : 아이템의 너비와 높이 */
                new Rect(   slotX + rectPosition.x + items[i].x * width + 4, 
                            slotY + rectPosition.y + items[i].y * height + 4, 
                            items[i].width * width - 8, 
                            items[i].height * height - 8), 
                items[i].image);
        }
    }

    /* 전체 인벤토리 순회 */
    public bool AddItem(Item item)
    {
        for(int x = 0; x < slotWidthSize; ++x)
        {
            for(int y = 0; y < slotHeightSize; ++y)
            {
                if(!OccupiedSlotSearch(x, y, item.width, item.height))
                {
                    AddItem(x, y, item);

                    return true;
                }
            }
        }

        return false;
    }

    /* 들어온 Rect를 기준으로 점거중인 슬롯이 있는지 확인 */
    private bool OccupiedSlotSearch(int x, int y, int widthX, int heightY)
    {
        for(int oX = x; oX < x + widthX; ++oX)
        {
            for(int oY = y; oY < y + heightY; ++oY)
            {
                if(oX >= slotWidthSize || oY >= slotHeightSize)
                {
                    return true;
                }

                if(slots[oX, oY].occupied)
                {
                    return true;
                }
            }
        }
        
        return false;
    }

    /* 원하는 위치만 검색 후 아이템을 추가 */
    public bool AddItem(int x, int y, Item item)
    {
        /* 슬롯에서 검색을 시도한 위치부터 아이템의 크기만큼 점거중인 아이템이 있는지 확인 */
        for(int sX = x; sX < item.width + x; ++sX)
        {
            for(int sY = y; sY < item.height + y; ++sY)
            {
                if(slots[sX, sY].occupied)
                {
                    Debug.Log("이미 다른 아이템이 점거 중인 슬롯입니다.");

                    return false;
                }
            }
        }

        /* 배열 범위 오류 검색 */
        if( x + item.width > slotWidthSize)
        {
            Debug.Log("Out of X bounds");
            
            return false;
        }
        else if( y + item.height > slotHeightSize)
        {
            Debug.Log("Out of Y bounds");

            return false;
        }     

        /* 추가한 아이템의 시작 위치 설정 */
        item.x = x;
        item.y = y;

        items.Add(item);

        /* 슬롯의 점거 상태 변경 */
        for(int sX = x; sX < item.width + x; ++sX)
        {
            for(int sY = y; sY < item.height + y; ++sY)
            {
                slots[sX,sY].occupied = true;
            }
        }

        Debug.Log("아이템 추가 완료.");

        return true;
    }

    /* 아이템을 제거하는 함수 */
    private void RemoveItem(Item item)
    {
        /* 아이템의 점거 상태를 false로 변경 */
        for(int x = item.x; x < item.x + item.width; ++x)
        {
            for(int y = item.y; y < item.y + item.height; ++y)
            {
                slots[x, y].occupied = false;
            }
        }

        items.Remove(item);
    }

    /* 마우스가 인벤토리 UI안에 있는지 확인 */
    private void DetectMouseOnGUI()
    {
        if(Input.mousePosition.x > rectPosition.x && Input.mousePosition.x < rectPosition.x + rectPosition.width)
        {
            if(Screen.height - Input.mousePosition.y > rectPosition.y && Screen.height - Input.mousePosition.y < rectPosition.y + rectPosition.height)
            {
                DetectMouseAction();

                return;
            }
        }
    }

    /* 마우스 클릭 감지 */
    private void DetectMouseAction()
    {
        for(int x = 0; x < slotWidthSize; ++x)
        {
            for(int y = 0; y < slotHeightSize; ++y)
            {
                Rect slot = new Rect(rectPosition.x + slots[x,y].rectPosition.x, rectPosition.y + slots[x,y].rectPosition.y, width, height);
                
                /* Point를 받아와 Rect안에 존재하는 지 판단 */
                if(slot.Contains(new Vector2(Input.mousePosition.x, Screen.height - Input.mousePosition.y)))
                {
                    if(Event.current.isMouse && Input.GetMouseButtonDown(0))
                    {
                        mouseDown.x = x;
                        mouseDown.y = y;
                    
                        /* 총 아이템 개수 만큼 반복 */
                        for(int index = 0; index < items.Count; ++index)
                        {
                            /* 아이템의 크기(x, y)만큼 반복, countX, Y : 아이템의 시작 위치 */
                             for(int countX = items[index].x; countX < items[index].x + items[index].width; ++countX)
                            {
                                for(int countY = items[index].y; countY < items[index].y + items[index].height; ++countY)
                                {
                                    /* 클릭한 위치와 아이템의 위치가 동일한 경우 */
                                    if(countX == x && countY == y)
                                    {
                                       tempItem = items[index];
                                       RemoveItem(tempItem);
                                       return;
                                    }
                                }
                            }   
                        }
                    }
                    else if(Event.current.isMouse && Input.GetMouseButtonUp(0))
                    {
                        mouseUp.x = x;
                        mouseUp.y = y;

                        /* 시작 클릭 위치와 종료 클릭 위치가 동일하지 않은 경우 == 아이템 이동을 시동한 경우 */
                        if(mouseUp.x != mouseDown.x || mouseUp.y != mouseDown.y)
                        {
                            if(tempItem != null)
                            {
                                /* mouseUp 위치에 아이템을 놓을 수 있으면 위치 변경 */
                                if(AddItem((int)mouseUp.x, (int)mouseUp.y, tempItem))
                                {

                                }
                                /* 변경 불가능한 경우 원래 위치로 이동 */
                                else
                                {
                                    AddItem(tempItem.x, tempItem.y, tempItem);
                                }
 
                                tempItem = null;
                            }
                        }
                        /* 아이템 이동을 시작하지 않은 경우 */
                        else
                        {
                            if(tempItem != null)
                            {
                                AddItem(tempItem.x, tempItem.y, tempItem);
                                tempItem = null;
                            }
                        }
                    }

                    //Debug.Log(mouseDown + "        " + mouseUp);

                    return;
                }
            }
        }
    }

    /* 임시 아이템을 그리는 함수 */
    private void DrawTempItem()
    {
        if(tempItem != null)
        {
            GUI.DrawTexture(new Rect(Input.mousePosition.x, Screen.height - Input.mousePosition.y, tempItem.width * width, tempItem.height * height), tempItem.image);
        }
    }
}

Inventory System

 

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

public class ItemDatabase : MonoBehaviour
{
    public List<Item> itemInspector;

    private static List<Item> itemList;

    private void Start() 
    {
        itemList = itemInspector;
    }

    public static Item GetItem(int id)
    {
        Item item = new Item();
        item.image = itemList[id].image;
        item.width = itemList[id].width;
        item.height = itemList[id].height;

        return item;
    }
}

ItemDatabase

 

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

/* 아이템 드롭 */
public class PhysicalItem : MonoBehaviour
{
    public static Inventory inventory;
    
    public int itemCode;
    
    private void OnMouseDown() 
    {
        inventory = Inventory.InventoryInstance().GetComponent<Inventory>();

        inventory.AddItem(ItemDatabase.GetItem(itemCode));
    }
}

PhysicalItem

블로그 이미지

NIA1995

,