<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>_hoya_.log</title>
        <link>https://velog.io/</link>
        <description>학습한 내용을 빠르게 다시 찾기 위한 저장소</description>
        <lastBuildDate>Sun, 01 Mar 2026 13:28:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>_hoya_.log</title>
            <url>https://velog.velcdn.com/images/_hoya_/profile/28f2980f-fef4-4072-843c-2697e84120bf/social_profile.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. _hoya_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/_hoya_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[올인원 서버 개발 후기]]></title>
            <link>https://velog.io/@_hoya_/AllInOne-Server-Review</link>
            <guid>https://velog.io/@_hoya_/AllInOne-Server-Review</guid>
            <pubDate>Sun, 01 Mar 2026 13:28:18 GMT</pubDate>
            <description><![CDATA[<p>시작은 채팅만 있는 서버였지만 클라이언트의 요구에 따라 점점 기능이 늘어나 나중에는 채팅, SNS, 게임, 광고, 지점관리까지 되는 올인원 서버를 구현하게 되었다.</p>
<hr>
<h2 id="구현한-부분">구현한 부분</h2>
<h3 id="웹소켓-부분">웹소켓 부분</h3>
<p>실시간 채팅 전송 및 실시간 게임 데이터 전달을 위한 웹소켓 연결 부분이다.
<img src="https://velog.velcdn.com/images/_hoya_/post/09bcb67d-211f-41b5-af9f-c9995d94a4fa/image.png" alt=""></p>
<h3 id="채팅-api-작업">채팅 API 작업</h3>
<p>카카오톡과 유사한 채팅 시스템을 구현하였다. 다음 기능들이 포함되어 있다.</p>
<ul>
<li>채팅방 시스템(생성, 초대, 접속, 탈퇴, 갱신 등)</li>
<li>채팅 시스템(문자, 이미지, 동영상 전송 및 삭제)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/0ec72acf-bd4b-4392-a1da-0d1e39c4784a/image.png" alt=""></p>
<h3 id="관계-api-작업">관계 API 작업</h3>
<p>사용자 친구요청, 차단 등 관계를 정의하는 시스템을 구현하였다.
<img src="https://velog.velcdn.com/images/_hoya_/post/b9e03fc0-1d48-42aa-804c-e3668b5c4ef7/image.png" alt=""></p>
<h3 id="sns-api-작업">SNS API 작업</h3>
<p>카카오스토리와 유사한 SNS 시스템을 구현하였다. 다음 기능들이 포함되어 있다.</p>
<ul>
<li>스토리 시스템(게시, 수정, 삭제)</li>
<li>팔로우 시스템(팔로우, 취소, 팔로우 게시글 가져오기)</li>
<li>검색(특정 유저, 특정 태그 검색)</li>
<li>답글 및 하트(게시글에 답글 및 하트 추가/삭제)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/09f5cd52-cf8f-43ac-af84-948be584ed7a/image.png" alt=""></p>
<h3 id="shorts-api-작업">Shorts API 작업</h3>
<p>유튜브 쇼츠와 유사한 Shorts 시스템을 구현하였다. 다음 기능들이 포함되어 있다.</p>
<ul>
<li>쇼츠 게시 및 관리(업로드, 수정, 삭제, 리스트 확인)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/b368b332-40e3-4377-914c-a7c1fe91e4e7/image.png" alt=""></p>
<h3 id="게임-api-작업">게임 API 작업</h3>
<p>가위바위보와 같은 미니게임 시스템을 구현하였다. 다음 기능들이 포함되어 있다.</p>
<ul>
<li>게임 프로필 및 랭킹 가져오기</li>
<li>게임 내 아이템 사용 및 추가</li>
</ul>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/0bc9242b-3ed3-4060-a0d9-b7be0e195520/image.png" alt=""></p>
<h3 id="광고-작업">광고 작업</h3>
<p>광고 조회 및 정산 요청 시스템을 구현하였다.
<img src="https://velog.velcdn.com/images/_hoya_/post/acc68e73-ffc5-402e-8d89-8249f593781b/image.png" alt=""></p>
<h3 id="관리자-작업">관리자 작업</h3>
<p>광고 및 정산 관리, 사용자 관리 및 지점 관리 API를 구현하였다.
<img src="https://velog.velcdn.com/images/_hoya_/post/58844007-86f9-4467-be57-cd4d900db989/image.png" alt=""><img src="https://velog.velcdn.com/images/_hoya_/post/8602c77c-3140-49eb-945a-b36f51afe091/image.png" alt=""></p>
<hr>
<h2 id="새로-학습한-부분">새로 학습한 부분</h2>
<h3 id="sql-정규화-및-join">SQL 정규화 및 join</h3>
<p>다양하고 넓은 데이터를 효율적으로 처리하기 위해 정규화와 Join을 많이 수행하였다.</p>
<h3 id="redis-캐싱">Redis 캐싱</h3>
<p>많이 사용하는 데이터들을 DB가 아닌 별도의 메모리에 저장하는 Redis 캐싱 또한 사용하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유연한 게임 데이터 스크립트]]></title>
            <link>https://velog.io/@_hoya_/flexible-gamedata-script</link>
            <guid>https://velog.io/@_hoya_/flexible-gamedata-script</guid>
            <pubDate>Thu, 18 Dec 2025 05:08:19 GMT</pubDate>
            <description><![CDATA[<h1 id="1-개요">1. 개요</h1>
<p>다량의 데이터가 필요하고 또 생산되는 타이쿤 게임의 서버 및 데이터 관리 직책을 맡게 되었다.
시간적 제한으로 인해 서버는 직접 개발 대신 상용 서버인 뒤끝 서버를 사용하기로 했다.</p>
<p>뒤끝 서버의 차트 시스템을 이용해 다량의 게임 데이터를 효과적으로 관리할 수 있게 되었지만, 이를 클라이언트에 주입하는 것은 다른 일이었다.</p>
<p>따라서 다음과 같은 시스템을 구현했다.
해당 시스템은 뒤끝 차트를 베이스로 작성하였지만, 다른 상용 서버나 클라이언트 내 csv파일 저장 등의 방식으로도 사용 가능하다.</p>
<p>이 문서에서는 뒤끝의 자세한 사용 방식과 이에 대한 스크립트는 작성하지 않고 스도코드로 대처한다.</p>
<h1 id="2-요구-조건-및-해결-방안">2. 요구 조건 및 해결 방안</h1>
<h2 id="요구-조건">요구 조건</h2>
<p>게임 데이터 스크립트의 요구조건은 다음과 같았다.</p>
<ul>
<li>A. 모든 게임 데이터의 이름/설명 등 필요한 부분은 모두 다국어로 번역이 가능해야 한다.</li>
<li>B. 게임 데이터에는 화면에 표기되는 데이터뿐 아니라 레벨별 경험치 등과 같이 단순 데이터로만 존재하는 종류도 있다.</li>
<li>C. 매니저 클래스에서 어떠한 데이터든 키 값으로 접근이 가능해야 한다.</li>
<li>D. 해당 게임 데이터는 그 게임데이터와 매칭되는 아이콘 및 프리팹 등을 갖고 있어야 한다.</li>
</ul>
<p>UI 표시를 위한 요구조건들이 주로 요구되었다.</p>
<h2 id="해결-방안">해결 방안</h2>
<h3 id="a-다국어-번역">A. 다국어 번역</h3>
<p>모든 설명의 다국어 번역 및 UI 표시를 위해 데이터의 종류를 크게 두 가지로 나누었다.</p>
<ul>
<li>게임 데이터 : 재화, 소모품 등 게임 내에서 상호작용하는 데이터</li>
<li>타입 데이터 : 각 게임 데이터를 분류하고 정의하는 데이터</li>
</ul>
<p>유니티에서 보편적으로 다국어 번역은 Unity Localization System을 사용한다. 해당 테이블은 테이블의 이름과 테이블 내 키값을 제시하면 이에 일치하는 문자열, Sprite, 오브젝트 등을 현재 유니티에서 설정된 언어에 맞춰 반환해준다.</p>
<p>게임 데이터 내에 각 데이터의 이름, 설명 등은 Localization 키 값들을 보유하며 번역을 반환해올 수 있지만, 해당 데이터들의 태그와 타입 등에 대한 Localization 키까지 게임 데이터 내에서 보유하고 있는 것은 저장공간 낭비라 판단했다.
따라서 타입들만을 모아둔 타입 데이터를 만들게 되었다.</p>
<h3 id="b레벨데이터와-같은-타입의-데이터">B.레벨데이터와 같은 타입의 데이터</h3>
<p>해당 데이터는 DataId와 DataType이 존재하지 않는다. 다만 딕셔너리에 넣기 위해서 <code>BaseData</code>라는 게임 데이터의 부모 클래스를 만들었다.</p>
<p>BaseData는 자유롭게 value값을 넣을 수 있지만, <code>string Id</code>를 필수적으로 가진다. 이는 <code>BaseGameData</code>에서는 DataId와 동일한 값을 갖는다.</p>
<h3 id="c매니저-클래스에서-모든-데이터의-접근">C.매니저 클래스에서 모든 데이터의 접근</h3>
<p>해당 조건을 충족하기 위해 모든 게임 데이터는 다음과 같은 값을 필수로 가진다.</p>
<ul>
<li>DataType : 매니저 클래스에서 해당 타입의 데이터들을 모아둔 딕셔너리에 접근하기 위한 키값</li>
<li>DataId : 해당 아이템의 고유값, 딕셔너리에서 해당 데이터를 찾기 위한 키값</li>
</ul>
<p>위의 두 값을 가지는 최상위 부모 클래스인 <code>BaseGameData</code>를 만든다. <code>GameDataBase</code>는 <code>BaseData</code>를 상속받으며 각각의 게임 데이터의 부모 클래스가 된다.</p>
<p>아이템 매니저 클래스에서는 각 ItemType의 데이터들을 ItemId를 Key로, GameDataBase를 상속받는 실제 데이터값을 Value로 하는 딕셔너리 형식으로 저장하고, 이 딕셔너리들을 Value로, 해당 아이템들의 ItemType을 키로 하는 딕셔너리에 참조하여 접근할 수 있게 한다.</p>
<h3 id="d게임데이터에-아이콘-및-프리팹-할당">D.게임데이터에 아이콘 및 프리팹 할당</h3>
<p>아이콘과 프리팹은 유니티의 Addressable Asset 시스템을 사용해 로드할 수 있게 해놓은다.
게임 데이터를 만들 때 ItemId를 바탕으로 뒤에 <code>_Icon</code>, <code>_Prefab</code> 등을 붙여 고유한 아이콘 및 프리팹 키를 선언하여 구현하였다.</p>
<h1 id="3-스도-코드">3. 스도 코드</h1>
<h2 id="스도코드-개요">스도코드 개요</h2>
<p>전체적인 스도 코드는 다음과 같다.</p>
<ul>
<li><code>GameDataManager</code> : 모든 게임 데이터에 접근할 수 있고 일괄적으로 관리할 수 있게 해주는 싱글턴 매니저 클래스</li>
<li><code>BaseData</code> : 모든 게임 데이터의 부모가 되는 베이스 클래스.<code>GameDataManager</code>에서 일괄적으로 관리하는 데이터의 형식이다.</li>
<li><code>BaseGameData</code> : 모든 화면에 표시되는 게임 데이터의 부모가 되는 베이스 클래스. 아이콘 혹은 프리팹을 가지고 다국어 번역이 필요한 이름 혹은 설명이 있기 때문에 ItemID와 ItemType뿐 아니라 로컬라이제이션과 어드레서블 관련 데이터들도 가지고 있는다.</li>
<li><code>~Data</code> : 실제로 사용하는 데이터들을 선언하는 자식 클래스이다.</li>
<li><code>TypeData</code> : 타입 및 태그 전용 데이터 클래스이다. 타입에 대한 로컬라이제이션을 지원한다.</li>
</ul>
<h2 id="상세-스도코드">상세 스도코드</h2>
<p>해당 스도코드는 각 스크립트가 하는 일을 번호순으로 순차적으로 나열하여 표현한다.</p>
<p>서버에서 차트 데이터를 받아오는 스크립트는 <code>Server.LoadChart(string chartName)</code>으로 간소화하였다.</p>
<h3 id="basedata">BaseData</h3>
<p>모든 게임 데이터의 부모 클래스이다. GameDataManager에서 일괄적으로 조회 및 검색하기 위해 사용되는 클래스 형식이다.</p>
<p>csv와 같이 단일 셀에 리스트 형식의 데이터를 넣기 위해 이를 파싱하는 함수를 구현한다.</p>
<pre><code>public abstract class BaseData
{
    // 딕셔너리 키용 ID, ObjData를 상속받으면 ObJData가 됨
    public string Id { get; protected set; } 


    // string 문자열을 List&lt;string&gt; 형식으로 파싱하는 함수
    public List&lt;string&gt; ParseString(string str)
    {
        List&lt;string&gt; res = new();

        str = str.Trim().TrimStart(&#39;[&#39;).TrimEnd(&#39;]&#39;);
        var parts = str.Split(&#39;,&#39;);

        foreach (var part in parts)
        {
            var clean = part.Trim().Trim(&#39;&quot;&#39;);
            res.Add(clean);
        }

        return res;
    }
}</code></pre><h3 id="gamedatabase">GameDataBase</h3>
<p>모든 화면에 보이는 게임 데이터가 필수적으로 가져야 하는 값들을 선언 및 할당한다.</p>
<pre><code>public abstract class GameDataBase : BaseData
{
    public string ItemId { get; private set; }              // 아이템 고유 ID
    public int ItemType { get; private set; }               // 아이템 타입
    public string IconPath { get; private set; }            // 아이템 아이콘 경로(어드레서블)
    public string PrefabPath { get; private set; }          // 아이템 프리팹 경로(어드레서블)
    public string LocaleTableName { get; private set;}        // 로케일 테이블 이름
    public string NameLocaleKey { get; private set; }       // 이름 로케일 키 (로컬라이제이션)
    public string DescLocaleKey { get; private set; }       // 설명 로케일 키 (로컬라이제이션)


    public Sprite Icon { get; protected set; }                // 아이템 아이콘
    public GameObject Prefab { get; protected set; }          // 아이템 프리팹


    // 생성자 클래스. 기본 값들을 할당해준다.
    public GameDataBase(string loTableName, LitJson.JsonData row)
    {
        ItemId = row[&quot;ItemID&quot;].ToString();
        ItemType = int.Parse(row[&quot;ItemType&quot;].ToString());

        IconPath = $&quot;{ItemId}_Icon&quot;;
        PrefabPath = $&quot;{ItemId}_Prefab&quot;;

        LocaleTableName = loTableName;
        NameLocaleKey = $&quot;{ItemId}_Name&quot;;
        DescLocaleKey = $&quot;{ItemId}_Desc&quot;;

        base.Id = ItemId;

        LoadIconSprite().Forget();
        LoadObjectPrefab().Forget();
    }


    // 이름 로컬라이제이션 스트링을 반환하는 함수
    public LocalizedString GetLocalName()
    {
        return new LocalizedString(LocaleTableName, NameLocaleKey);
    }


    // 설명 로컬라이제이션 스트링을 반환하는 함수
    public LocalizedString GetLocalDesc()
    {
        return new LocalizedString(LocaleTableName, DescLocaleKey);
    }


    // 아이콘을 로드하는 함수
    public async UniTask LoadIconSprite()
    {
        if (Icon != null) return;   // 이미 있으면 종료

        Icon = await AddressableManager.I.GetAssetAsync&lt;Sprite&gt;(IconPath, caching);
        if (Icon == null)
        {
            Debug.LogError($&quot;Failed to load icon of {ItemType.ToString()} - {ItemId}&quot;);
        }
    }


    // 에셋을 로드하는 함수
    public async UniTask LoadObjectPrefab(bool caching = false)
    {
        if (Prefab != null) return;   // 이미 있으면 종료

        Prefab = await AddressableManager.I.GetAssetAsync&lt;GameObject&gt;(PrefabPath, caching);
        if (Prefab == null)
        {
            Debug.LogError($&quot;Failed to load prefab of {ItemType.ToString()} - {ItemId}, Loading Temp Prefab&quot;);
        }
    }
}</code></pre><h3 id="data">~Data</h3>
<p>실제 사용되는 게임 데이터를 구현해둔 클래스이다.</p>
<pre><code>public class ItemBoxData : GameDataBase
{
    public int BoxType;             // 박스의 타입
    public List&lt;string&gt; BoxItemId;     // 박스 아이템이 포함하는 아이템 ID 리스트
    public List&lt;int&gt; Num;             // 박스 아이템이 포함하는 아이템 개수 리스트

    public ItemBoxData(string loTableName, LitJson.JsonData row) : base(loTableName, row)
    {
        BoxType = int.Parse(row[&quot;BoxType&quot;].ToString());
        BoxItemId = ParseString(row[&quot;BoxItemID&quot;].ToString());
        Num = ParseString(row[&quot;Num&quot;].ToString()).ConvertAll(s =&gt; int.Parse(s));
    }
}</code></pre><h3 id="typedata">TypeData</h3>
<p>타입들을 위한 데이터이다. enum과 유사하게 int를 키로, string을 value로 하여 타입 데이터를 지정하고, 번역을 위한 로컬라이제이션을 연결하였다.</p>
<p>로컬라이제이션에 해당 키가 있는지 확인하는 함수를 구현하였다.</p>
<pre><code>public class TypeData
{
    private string _tableName;            // 로컬라이제이션 테이블 이름
    private string _chartName;            // 데이터 로드용 차트 이름

    private Dictionary&lt;int, string&gt; _data;        // 데이터


    // 생성자
    public TypeData(string chartName, string tableName)
    {
        _chartName = chartName;
        _tableName = tableName;
        _data = new Dictionary&lt;int, string&gt;();
    }

    public string GetValue(int id)
    {
        if (_data.ContainsKey(id))
        {
            return _data[id];
        }
        return null;
    }

    public Dictionary&lt;int, string&gt; GetValues()
    {
        return _data;
    }

    // 해당 타입의 번역을 반환하는 함수
    public LocalizedString getLocalString(int id)
    {
        if (_data.ContainsKey(id))
        {
            return new LocalizedString(_tableName, _data[id]);
        }
        return null;
    }


    // 타입 데이터 초기화 함수
    public async void InitTypes()
    {
        var rows = Server.LoadChart(_chartName);
        foreach (LitJson.JsonData row in rows)
        {
            _data.Add(int.Parse(row[&quot;Key&quot;].ToString()), row[&quot;Value&quot;].ToString());
        }

        await CheckKeysExistInTableAsync(_data.Values.ToList());
    }


    // 로컬라이제이션 테이블과 키 매칭 여부 함수
    private async UniTask CheckKeysExistInTableAsync(List&lt;string&gt; keys)
    {
        await LocalizationSettings.InitializationOperation.Task;

        // StringTableCollection이 아닌, Runtime에서 Table을 불러옴
        var table = await LocalizationSettings.StringDatabase.GetTableAsync(_tableName);

        if (table == null)
        {
            Debug.LogError($&quot;테이블 &#39;{_tableName}&#39;을 찾을 수 없습니다.&quot;);
            return;
        }

        // StringTable의 ContainsKey 사용
        foreach (var key in keys)
        {
            var entry = table.GetEntry(key);
            if (entry == null)
                Debug.LogError($&quot;{_tableName}의 {key}를 찾을 수 없습니다.&quot;);
        }
    }</code></pre><h3 id="gamedatamanager">GameDataManager</h3>
<p>타입 데이터와 게임 데이터들을 관리하는 매니저 클래스이다.</p>
<p>로컬라이제이션 키 매칭 여부 체크 함수를 구현하여 초기화 이후 체킹해준다.</p>
<pre><code>public class GameDataManager : MonoBehaviour
{
    public static GameDataManager I { get; private set; }

    // 게임 데이터 일괄 관리용 딕셔너리
    private readonly Dictionary&lt;int, object&gt; _dataMap = new();

    // 아이템박스 타입 데이터
    public TypeData ItemBoxType { get; private set; } = new(&quot;ItemBoxType&quot;, &quot;Type_ItemBoxTypeTable&quot;);      

    // 아이템박스 게임 데이터
    public SerializedDictionary&lt;string, ItemBoxData&gt; ItemBoxData { get; private set; } = new();


    // 전체 게임 데이터 초기화 함수
    public async UniTask InitGameData()
    {
        // 타입 데이터 초기화
        ItemBoxType.InitTypes();

        // 게임 데이터 초기화
        await InitGameDatas&lt;ItemBoxData&gt;(&quot;ItemBoxChart&quot;, ItemBoxData, row =&gt; new ItemBoxData(row));

        // 초기화한 게임 데이터를 관리 딕셔너리에 매핑
        _dataMap[2] = ItemBoxData;


        // 로컬라이제이션 키 매칭 확인
#if UNITY_EDITOR
        // 타입 데이터 매칭 확인
        await CheckKeysExistInTableAsync(&quot;Type_ItemBoxTypeTable&quot;, ItemBoxType.GetValues().Values.ToList());
        // 게임 데이터 매칭 확인
        await CheckKeysExistInTableAsync(&quot;ItemBoxTable&quot;, ItemBoxData.Values.ToList());
#endif
    }


    // 특정 타입과 ID에 해당하는 게임 데이터를 반환하는 함수
    public T GetData&lt;T&gt;(int type, string id) where T : BaseData
    {
        if (_dataMap.TryGetValue(type, out var dict) &amp;&amp; dict.TryGetValue(id, out var data))
            return data as T;

        Debug.LogError($&quot;Data not found for type={type}, id={id}&quot;);
        return null;
    }


    // 게임 데이터 초기화 함수(팩토리 메서드 사용)
    private async UniTask InitGameDatas&lt;T&gt;(string chartName, Dictionary&lt;string, T&gt; target ,Func&lt;LitJson.JsonData, T&gt; factory) where T : BaseData
    {
        var rows = Server.LoadChart(chartName);
        foreach (LitJson.JsonData row in rows)
        {
            T data = factory(row);
            target.Add(data.Id, data);
        }
    }

    // 게임 데이터에 필요한 로컬라이제이션이 로컬라이제이션 테이블에 존재하는지 체크하는 함수
    private async UniTask CheckKeysExistInTableAsync&lt;T&gt;(string tableName, List&lt;T&gt; Data, bool CheckDesc = true) where T : ObjData
    {
        await LocalizationSettings.InitializationOperation.Task;

        // StringTableCollection이 아닌, Runtime에서 Table을 불러옴
        var table = await LocalizationSettings.StringDatabase.GetTableAsync(tableName);

        if (table == null)
        {
            Debug.LogError($&quot;테이블 &#39;{tableName}&#39;을 찾을 수 없습니다.&quot;);
            return;
        }

        // StringTable의 ContainsKey 사용
        foreach (var data in Data)
        {
            var entry1 = table.GetEntry(data.NameLocaleKey);
            if (entry1 == null)
                Debug.LogError($&quot;{tableName}의 {data.NameLocaleKey}를 찾을 수 없습니다.&quot;);
            if (CheckDesc)
            {
                var entry2 = table.GetEntry(data.DescLocaleKey);
                if (entry2 == null)
                    Debug.LogError($&quot;{tableName}의 {data.DescLocaleKey}를 찾을 수 없습니다.&quot;);
            }
        }
    }


    // 타입 데이터에 필요한 로컬라이제이션이 로컬라이제이션 테이블에 존재하는지 체크하는 함수
    private async UniTask CheckKeysExistInTableAsync(string tableName, List&lt;string&gt; keys)
    {
        await LocalizationSettings.InitializationOperation.Task;
        // StringTableCollection이 아닌, Runtime에서 Table을 불러옴
        var table = await LocalizationSettings.StringDatabase.GetTableAsync(tableName);
        if (table == null)
        {
            Debug.LogWarning($&quot;테이블 &#39;{tableName}&#39;을 찾을 수 없습니다.&quot;);
            return;
        }
        // StringTable의 ContainsKey 사용
        foreach (var key in keys)
        {
            var entry = table.GetEntry(key);
            if (entry == null)
                Debug.LogWarning($&quot;{tableName}의 {key}를 찾을 수 없습니다.&quot;);
        }
    }
}</code></pre><h1 id="후기-및-추가-팁">후기 및 추가 팁</h1>
<p>로컬라이제이션과 어드레서블 둘 다 string key를 이용해 접근할 수 있엇던 것이 참 다행이다.</p>
<p>BaseGameData에서 로컬과 어드레서블에 대해 각각의 key들을 선언하고 할당해 주는데, 이들은 
Id에서 파생되는 데이터이므로 그냥 각각의 Get 함수에서 하드코딩을 해 부르는 것이 메모리를 아끼는 데에 더 도움이 되었을 것 같긴 하다.</p>
<p>GameDataManager의 GetData 함수는 int type이라는 키가 아니라 Type형의 키를 사용해 해당 데이터 클래스를 제네릭에 추가하면 자연스럽게 id만 사용해서 가져올 수 있지 않을까 고민했는데,
csv나 엑셀 시트로 만들어지는 차트에는 typeof(Type)보다 미리 지정해 둔 int형 타입을 사용하는 것이 훨씬 편하다는 것을 깨달아 단순하게 구현하였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티에서 SOAP API 보내기]]></title>
            <link>https://velog.io/@_hoya_/Unity-Soap-Api</link>
            <guid>https://velog.io/@_hoya_/Unity-Soap-Api</guid>
            <pubDate>Mon, 15 Dec 2025 05:56:28 GMT</pubDate>
            <description><![CDATA[<h1 id="soap-api">SOAP API</h1>
<p><a href="https://reqbin.com/soap-api-testing">SOAP API testing tool</a></p>
<h2 id="soap-api란">SOAP API란?</h2>
<p>XML 기반의 메시징 프로토콜로, 주로 엔터프라이즈 환경에서 시스템 간 통신을 위해 사용된다. REST API보다 더 엄격한 표준과 보안 기능을 제공하는 것이 특징이다.</p>
<p>SOAP API는 다음과 같은 특징을 가진다.</p>
<ul>
<li>프로토콜 기반 : REST는 아키텍처 스타일인 반면, SOAP는 공식적인 프로토콜(규약)이다.</li>
<li>XML 전용 : 메시지 형식으로 오직 XML만을 사용하며, 요청과 응답 모두 XML 문서 구조를 따른다.</li>
<li>엄격한 보안 : WS-Security와 같은 자체 보안 표준을 지원하며, 높은 수준의 보안(예: 금융 거래)이 요구될 때 유리하다.</li>
<li>상태 유지 가능: 경우에 따라 요청 간에 상태 정보를 유지할 수 있어, 복잡한 트랜잭션 처리에 적합하다.</li>
<li>데이터 변형 방지 : 자체적인 ACID (데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질) 기준이 있어 데이터의 변형을 줄여준다.</li>
<li>플랫폼 독립성 : HTTP뿐만 아니라 SMTP, TCP 등 다양한 전송 프로토콜 위에서 동작할 수 있어 플랫폼이나 언어에 구애받지 않는다.</li>
</ul>
<p>강력한 보안성과 데이터 변형 방지로 인해 주로 은행이나 기업의 보안 절차에서 사용하고 있다.</p>
<h2 id="soap-api의-형식">SOAP API의 형식</h2>
<ul>
<li>Envelop(필수) : SOAP 메시지 전체를 감싸는 루트(Root) 요소입니다. XML 네임스페이스를 정의하며, 메시지의 시작과 끝을 나타낸다.</li>
<li>Header(선택) : 실제 비즈니스 로직과는 별개인 부가 정보(예: 보안 인증 토큰, 라우팅 정보, 트랜잭션 ID)를 포함한다.</li>
<li>Body(필수) : 웹 서비스가 제공하는 실제 호출 내용(메서드 이름, 매개변수, 반환 값 등)이 들어간다.</li>
<li>Fault(선택) : Body 내에 위치하며, 요청 처리 중 오류가 발생했을 때 예외 정보를 반환하는 데 사용된다.</li>
</ul>
<pre><code>&lt;soap:Envelope xmlns:soap=&quot;schemas.xmlsoap.org&quot;
               xmlns:m=&quot;www.example.org&quot;&gt;
    &lt;soap:Header&gt;
        &lt;!-- 보안 인증 정보 등을 여기에 포함시킬 수 있습니다. --&gt;
        &lt;m:AuthToken&gt;SAMPLE_TOKEN_12345&lt;/m:AuthToken&gt;
    &lt;/soap:Header&gt;
    &lt;soap:Body&gt;
        &lt;!-- m 네임스페이스의 GetStockPrice 메서드를 호출합니다. --&gt;
        &lt;m:GetStockPrice&gt;
            &lt;!-- Symbol이라는 매개변수에 IBM 값을 전달합니다. --&gt;
            &lt;m:Symbol&gt;IBM&lt;/m:Symbol&gt;
        &lt;/m:GetStockPrice&gt;
    &lt;/soap:Body&gt;
&lt;/soap:Envelope&gt;</code></pre><h2 id="유니티에서-soap-보내기">유니티에서 SOAP 보내기</h2>
<p>유니티는 SOAP API를 공식적으로 지원하지 않는다.
다만 <code>UnityWebRequest</code>를 이용해 API 서버에 패킷을 전송하듯이 전송하고 받을 수 있다.
파싱은 기존의 REST API에서 사용하던 json 대신 xml로 치환하면 가능하다.</p>
<pre><code>    private const string SOAP_URL = &quot;https://test.com/test/url&quot;; // soap url
    private const string SOAP_ACTION = &quot;http://tempuri.org/testAction&quot;; // soap method

    public async UniTask&lt;XmlNodeList&gt; Call_SoapTest()
    {
        // body 만들기
        string soapEnvelope =
$@&quot;&lt;soap:Envelope xmlns:soap=&quot;schemas.xmlsoap.org&quot;
               xmlns:m=&quot;www.example.org&quot;&gt;
    &lt;soap:Header&gt;
        &lt;!-- 보안 인증 정보 등을 여기에 포함시킬 수 있습니다. --&gt;
        &lt;m:AuthToken&gt;SAMPLE_TOKEN_12345&lt;/m:AuthToken&gt;
    &lt;/soap:Header&gt;
    &lt;soap:Body&gt;
        &lt;!-- m 네임스페이스의 GetStockPrice 메서드를 호출합니다. --&gt;
        &lt;m:GetStockPrice&gt;
            &lt;!-- Symbol이라는 매개변수에 IBM 값을 전달합니다. --&gt;
            &lt;m:Symbol&gt;IBM&lt;/m:Symbol&gt;
        &lt;/m:GetStockPrice&gt;
    &lt;/soap:Body&gt;
&lt;/soap:Envelope&gt;&quot;;
        // 만든 body를 byte[]로 변환
        byte[] bodyRaw = new UTF8Encoding(false).GetBytes(soapEnvelope);

        // UnityWebRequest로 패킷 준비
        UnityWebRequest request = new UnityWebRequest(SOAP_URL, &quot;POST&quot;);
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        request.downloadHandler = new DownloadHandlerBuffer();

        // 패킷에 헤더 추가
        request.SetRequestHeader(&quot;Content-Type&quot;, &quot;text/xml; charset=utf-8&quot;);
        request.SetRequestHeader(&quot;SOAPAction&quot;, SOAP_ACTION);

        // 패킷 전송
        await request.SendWebRequest().ToUniTask();

        // 패킷 전송 성공시
        if (request.result == UnityWebRequest.Result.Success)
        {
            // 결과 문자열 확인
            string responseXml = request.downloadHandler.text;
            Debug.Log(responseXml);

            // xml 형식으로 파싱
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(responseXml);
            XmlNodeList list = doc.GetElementsByTagName(&quot;AdAuthCheckEncryptResult&quot;);

            return list;
        }
        else
        {
            Debug.LogError($&quot;SOAP Error: {request.error}&quot;);
            return null;
        }
    }</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[유연한 플레이어 데이터 스크립트]]></title>
            <link>https://velog.io/@_hoya_/flexible-playerdata-script</link>
            <guid>https://velog.io/@_hoya_/flexible-playerdata-script</guid>
            <pubDate>Mon, 08 Dec 2025 15:50:36 GMT</pubDate>
            <description><![CDATA[<h1 id="1-개요">1. 개요</h1>
<p>다량의 데이터가 필요하고 또 생산되는 타이쿤 게임의 서버 및 데이터 관리 직책을 맡게 되었다.
시간적 제한으로 인해 서버는 직접 개발 대신 상용 서버인 <a href="https://backnd.com/ko/">뒤끝 서버</a>를 사용하기로 했다.</p>
<p>뒤끝 서버의 차트 시스템을 이용해 다량의 게임 데이터를 효과적으로 관리할 수 있게 되었지만
게임 데이터로부터 파생되는 수많은 플레이어 데이터들을 효율적으로 관리할 필요가 있었다.</p>
<p>따라서 다음과 같은 시스템을 구현했다.
해당 시스템은 뒤끝 서버를 베이스로 작성하였지만, Firebase DataBase와 같은 다른 상용 서버에서도 사용 가능하다.</p>
<p>이 문서에서는 뒤끝의 자세한 사용 방식과 이에 대한 스크립트는 작성하지 않고 스도코드로 대처한다.</p>
<h1 id="2-요구-조건-및-해결-방안">2. 요구 조건 및 해결 방안</h1>
<h2 id="요구-조건">요구 조건</h2>
<p>해당 플레이어 데이터 스크립트의 요구조건은 다음과 같았다.</p>
<ul>
<li>서버 및 로컬에 원할 때 저장 및 불러오기가 가능해야 한다.</li>
<li>어떠한 데이터 타입이어도 저장 및 불러오기가 가능해야 한다.</li>
</ul>
<p>간단하지만 참으로 파괴적인 요구 조건이었다. 원래는 서버에만 데이터를 저장하려 했지만 </p>
<ul>
<li>실시간으로 데이터가 확확 바뀌는 타이쿤 게임의 특성</li>
<li>뒤끝 DB 시스템의 부족한 칼럼으로 인해 데이터를 통째로 업로드 해야 함</li>
</ul>
<p>위와 같은 제약으로 인해 서버 유지비용이 천정부지로 솟게 되어
&#39;로컬 저장 우선&#39; + &#39;원할 때 서버에 데이터 저장 및 불러오기 기능&#39;을 구현하게 되었다.
다만 유료재화의 수량과 일일보상 수령 상황 등의 중요 데이터는 서버에만 저장하고 불러오게 구현하였다.</p>
<h2 id="해결-방안">해결 방안</h2>
<p>&#39;서버 및 로컬에 저장 및 불러오기&#39;는 감당할 수 없는 데이터 전송 비용을 방지하기 위해 다음과 같은 방식으로 구현하였다.</p>
<ol>
<li>실시간 변경은 항상 로컬에 저장</li>
<li>중요한 변경점 생성 시에만 서버에 저장</li>
<li>사용자가 원할 시 일괄적으로 서버에 저장 및 불러오기</li>
</ol>
<p>최종적으로는 &#39;젤다의 전설&#39;과 같은 콘솔 게임의 저장 방식을 따르게 되었다.
솔로 게임이어서 평소에는 로컬에 저장하되, 과금 재화 등의 중요한 정보는 서버에 즉시 저장하고, 모바일 게임 특성 상 기기변경이 일어날 수 있어 기기 이동을 도와주는 일괄 저장 및 불러오기 기능을 추가하기로 결정했다.
서버에 저장 시에는 항상 로컬 저장을 동시에 수행하여 서버 데이터가 로컬 데이터보다 최신화되지 않도록 하였다.</p>
<p>&#39;어떠한 데이터 타입이어도 저장 및 불러오기가 가능&#39; 문제는 <code>Param</code>과 <code>LitJson</code> 시스템을 사용하여 구현할 수 있었다.</p>
<ul>
<li><code>Param</code>은 뒤끝과 같은 상용 서버에서 데이터를 업로드할 때 일괄적으로 데이터를 전송할 수 있는 객체로 <code>Json string</code>형식으로의 전환이 간단한 <code>Dictionary&lt;string, object&gt;</code> 형식의 객체이다.</li>
<li><code>LitJson</code>은 데이터를 다운로드할 때 전송해주는 데이터 형식이며 이 또한 <code>Json string</code>과 <code>Dictionary&lt;string, object&gt;</code> 형식으로의 전환이 간단한 객체이다.</li>
</ul>
<p>객체를 가져올 때는 서버에서 전송해주는 <code>Json</code>데이터를 파싱하여 사용한다. 로컬 데이터 또한 <code>Json</code>으로 저장 및 불러오기를 하면 간단하게 구현 가능하다.</p>
<p>다음은 뒤끝 서버에서의 데이터 전송 및 불러오기의 예시 코드이다.</p>
<pre><code>// Param에 값 추가
param.Add(&quot;TotalGoldEarned&quot;, TotalGoldEarned);

// LitJson에서 값 가져오기
TotalGoldEarned = data.ContainsKey(&quot;TotalGoldEarned&quot;) ? long.Parse(data[&quot;TotalGoldEarned&quot;].ToString()) : 0;</code></pre><h1 id="3-스도-코드">3. 스도 코드</h1>
<h2 id="스도코드-개요">스도코드 개요</h2>
<p>전체적인 스도 코드는 다음과 같다. 다음과 같이 나뉘어진다.</p>
<ul>
<li><code>PlayerDataManager</code> : 모든 플레이어 데이터에 접근할 수 있고 일괄적으로 관리할 수 있게 해주는 싱글턴 매니저 클래스</li>
<li><code>IPlayerData</code> : 로컬 저장, 서버 저장, 데이터 불러오기, 데이터 초기화 등 모든 데이터 클래스가 수행할 함수들을 모아둔 인터페이스. 해당 인터페이스의 함수들은 <code>PlayerDataManager</code>에서 데이터를 일괄 관리할 때 사용하기도 한다.</li>
<li><code>PlayerDataBase</code> : <code>IPlayerData</code>의 함수들을 실제로 구현하고, 각 플레이어 데이터의 추가 및 제거 등을 도와주는 abstract 함수들을 선언한다.</li>
<li><code>PlayerData</code> : 실제로 사용하는 데이터들을 선언하고 해당 데이터의 추가 및 제거에 대한 기능들을 구현한다.</li>
</ul>
<h2 id="상세-스도코드">상세 스도코드</h2>
<p>해당 스도코드는 각 스크립트가 하는 일을 번호순으로 순차적으로 나열하여 표현한다.</p>
<p>서버와 데이터를 주고받는 스크립트는 <code>Server.LoadData()</code>와 <code>Server.SaveData(value)</code>로 간소화하였다.
서버에 데이터를 초기화하는 함수는 <code>Server.InitData(value)</code>로 간소화하였다.</p>
<p>로컬에 데이터를 저장하고 불러오는 스크립트는 <code>Local.LoadData()</code>와 <code>Local.SaveData(value)</code>로 간소화하였다.</p>
<h3 id="iplayerdata">IPlayerData</h3>
<p><code>PlayerDataManager</code>에서 일괄적으로 사용할 함수들만을 넣어두었다.
로컬 데이터를 불러오는 함수가 구현되지 않은 이유는 선언 시 기본적으로 로컬 데이터를 불러오기 때문이다.</p>
<pre><code>public interface IPlayerData
{
    // 로컬 저장소에 데이터를 저장하는 함수
    bool SaveDataAtLocal();

    // 서버에 데이터를 저장하는 함수
    bool SaveDataAtServer();


    // 로컬에서 데이터를 가져오는 함수
    bool LoadDataFromLocal();

    // 서버에서 데이터를 가져오는 함수
    bool LoadDataFromServer();


    // 로컬 데이터를 초기화하는 함수
    bool ResetLocalData();

    // 로컬과 서버 전체 데이터를 초기화하는 함수
    bool ResetData(); 
}</code></pre><h3 id="playerdatabase">PlayerDataBase</h3>
<p><code>IPlayerData</code>에서 선언한 함수들을 상세히 구현하고, 이를 상속받는 자식 클래스들에서 어떠한 데이터형이든 대응될 수 있게 자식 클래스들에서 구체화할 abstract 함수들을 작성한다.</p>
<pre><code>public class PlayerDataBase : IPlayerData
{
    // 해당 데이터를 서버에 저장하는 DB 테이블의 이름, 로컬 저장 시에도 파일의 이름으로 사용 가능하다.
    protected string TableName = string.Empty;


    // 클래스 생성자. TableName을 받아 이를 바탕으로 로컬 혹은 서버에서 데이터를 초기화한다.
    public PlayerDataBase(string tableName)
    {
        // 1. 테이블 값 입력
        TableName = tableName;

        // 2. 로컬에서 데이터 가져오기, 실패 시 서버에서 데이터 가져오기
        if (LoadDataFromLocal())
            return;

        LoadDataFromServer();
    }


// 다음 region에서는 자식 클래스에서 각각 저장되는 데이터 형에 맞춰 구현해야 하는 abstract 클래스만을 선언한다.
#region Abstract Data Methods


    // 클래스 내 모든 데이터를 Param에 저장하여 반환하는 함수
    protected abstract Param GetAllDataParam();


    // 서버에서 받아온 LitJson을 클래스 내에 각각의 값으로 할당해주는 함수
    protected abstract void SetData(LitJson.JsonData data);


    // 클래스 내 모든 데이터들에 대해 개발자가 임의로 정한 기본값으로 세팅해주는 함수
    private abstract void SetDefaultData();


#endregion


// 다음 region에는 IPlayerData에서 선언한 저장 및 불러오기 함수들과 이에 대한 헬퍼 함수들의 구현부를 작성한다.
#region Save/Load Methods


    // 서버에 처음으로 데이터를 추가할 때 사용하는 함수
    private bool InitDataAtServer(Param param = null)
    {
        // 1. 서버 초기화. 실패 시 false 반환
        bool result = Server.InitData(param);
        if (result == false)
            return false;

        // 2. 서버에서 방금 초기화한 데이터를 로드
        return LoadDataFromServer();
    }


    // 서버에서 데이터를 받아와 로드할 때 사용하는 함수
    protected bool LoadDataFromServer()
    {
        // 1. 서버에서 데이터 받아오기. 실패 시 데이터 추가
        var data = Server.LoadData();
        if (data == nullOrEmpty)
            return InitDataAtServer(GetDefaultValueParam());

        // 2. 받아온 데이터가 있으면 값을 할당해주고 true 반환
        SetData(data);
        return true;
    }


    // 로컬에서 데이터를 받아와 로드할 때 사용하는 함수
    protected bool LoadDataFromLocal()
    {
        // 1. TableName이 없으면 오류이므로 false 반환
        if (string.IsNullOrEmpty(TableName))
            return false;

        // 2. 로컬에서 데이터 가져오기. 실패 시 SetDefaultData() 수행 후 false 반환
        var localData = Local.LoadData();
        if (localData == null || localData == default)
        {
            SetDefaultData();
            return false;
        }

        // 3. 성공 시 저장된 데이터 확인 후 데이터 설정
        var data = JsonConvert.JsonToObj(localData)
        if (data == nullOrEmpty)
            SetDefaultData();
        else
            SetData(data);

        return true;
    }


    // 로컬 저장소에 데이터를 저장하는 함수
    public bool SaveDataAtLocal()
    {
        // 1. 전체 데이터를 가져와 Json string으로 직렬화한다.
        string jsonData = JsonConvert.ObjToJson(GetAllDataParam());

        // 2. 직렬화한 데이터를 저장한다.
        return Local.SaveData(jsonData);
    }


    // 서버에 데이터를 저장하는 함수
    public bool SaveDataAtServer()
    {
        // 1. 우선 로컬에 데이터를 저장한다. 실패 시에는 즉시 종료한다.
        if (!SaveDataAtLocal())
            return false;

        // 2. 전체 데이터를 가져온다.
        var data = GetAllDataParam();

        // 3. 서버에 데이터를 저장한다.
        return Server.SaveData(data);
    }


    // 로컬 데이터를 초기화하는 함수
    bool ResetLocalData()
    {
        // 1. 기본 데이터로 초기화한다.
        SetDefaultData();

        // 2. 로컬 저장소의 파일을 삭제한다.
        return FileManager.DeleteFile(TableName);
    }


    // 로컬과 서버 전체 데이터를 초기화하는 함수
    bool ResetData()
    {
        // 1. 로컬 데이터를 초기화한다.
        if (!ResetLocalData())
            return false;

        // 2. 초기화된 데이터를 서버에 업로드한다.
        var data = GetAllDataParam();
        return Server.SaveData(data);
    }
}

#endregion</code></pre><h3 id="playerdata">~PlayerData</h3>
<p>실제 데이터가 선언되어 있는 클래스. <code>PlayerDataBase</code>에서 abstract로 선언했던 함수들을 선언한 데이터 형식에 맞게 구현한다.
또한 각 데이터의 CRUD 도움 함수들도 이곳에서 각각 구현한다.</p>
<p>아래는 예시 클래스이다. Param의 추가 및 LitJson의 수행은 뒤끝 서버에서의 데이터 전송 및 불러오기 예시를 바탕으로 구현하였다.</p>
<pre><code>public class TestPlayerData : PlayerBaseData
{
    public int TestInt;
    public Dictionary&lt;string, string&gt; TestDic;

    public TestPlayerData(string tableName) : base(tableName) {}


    protected Param GetAllDataParam()
    {
        Param param = new Param();

        param.Add(&quot;TestInt&quot;, TestInt);
        param.Add(&quot;TestDic&quot;, JsonConvert.ObjToJson(TestDic));

        return param;
    }


    protected void SetData(LitJson.JsonData data)
    {
        TestInt = data.ContainsKey(&quot;TestInt&quot;) ? int.Parse(data[&quot;TestInt&quot;].ToString()) : 0;
        TestDic = data.ContainsKey(&quot;TestDic&quot;) ? JsonConvert.JsonToObj(data[&quot;TestDic&quot;].ToString()) : new();
    }


    private void SetDefaultData()
    {
        TestInt = 0;
        TestDic = new();
    }
}</code></pre><h3 id="playerdatamanager">PlayerDataManager</h3>
<p>각각의 PlayerData들을 선언하여 초기화하고, 일괄적으로 관리할 수 있게 해주는 클래스이다.</p>
<pre><code>public class PlayerDataManager : SingleTon
{
    // 모든 데이터를 일괄적으로 관리하기 위해 선언하는 딕셔너리
    private Dictionary&lt;string, IPlayerData&gt; _dict = new();

    // 플레이어 데이터 모음
    public TestPlayerData testData;
    public Test2PlayerData test2Data;


    // 데이터 초기화 함수
    public void Init()
    {
        testData = new TestPlayerData(&quot;TestPlayerData&quot;);
        _dict.Add(&quot;TestPlayerData&quot;, testData);

        test2Data= new Test2PlayerData(&quot;Test2PlayerData&quot;);
        _dict.Add(&quot;Test2PlayerData&quot;, test2Data);
    }


    // 로컬 일괄 저장 함수
    public void SaveAllDataAtLocal()
    {
        foreach (var data in _dict) { data.Value.SaveDataAtLocal(); }
    }


    // 서버 일괄 저장 함수
    public void SaveAllDataAtServer()
    {
        foreach (var data in _dict) { data.Value.SaveDataAtServer(); }
    }


    // 로컬 일괄 불러오기 함수
    public void LoadAllDataFromLocal()
    {
        foreach (var data in _dict) { data.Value.LoadDataFromLocal(); }
    }


    // 서버 일괄 불러오기 함수
    public void LoadAllDataFromServer()
    {
        foreach (var data in _dict) { data.Value.LoadDataFromServer(); }
    }


    // 로컬 일괄 초기화 함수
    public void ResetAllLocalData()
    {
        foreach (var data in _dict) { data.Value.ResetLocalData(); }
    }


    // 로컬과 서버 전체 데이터 일괄 초기화 함수
    public void ResetAllData()
    {
        foreach (var data in _dict) { data.Value.ResetAllData(); }
    }
}</code></pre><h1 id="후기-및-추가-팁">후기 및 추가 팁</h1>
<p>해당 코드들을 설계하여 데이터들을 자유롭게 생성할 수 있게 됨으로써 시간 절약을 크게 할 수 있었다.</p>
<p>위의 스도코드에는 작성하지 않았지만, <code>PlayerClassData&lt;T&gt; : PlayerBaseData</code>와 같이 제네릭 클래스를 데이터로 추가하여 관리할 수도 있다. 이는 각각의 고유 데이터를 필요로 하는 직원 시스템 등에 유용하게 사용했다.</p>
<p>또한 .csv를 이용한 게임 데이터 차트 시스템과 연계하여 PlayerData 안에서 원본 GameData까지 가져오게 하는 방법 또한 충분히 가능하다. 이 때는 <code>PlayerClassData&lt;G, T&gt; : PlayerBaseData</code>와 같이 선언하여 <code>G</code>에 GameData 클래스 형식을 선언하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[.net 웹서버 팁]]></title>
            <link>https://velog.io/@_hoya_/.net-.Webserver-Tip</link>
            <guid>https://velog.io/@_hoya_/.net-.Webserver-Tip</guid>
            <pubDate>Mon, 01 Sep 2025 03:11:23 GMT</pubDate>
            <description><![CDATA[<h2 id="1-로깅-정책-작성">1. 로깅 정책 작성</h2>
<h3 id="기본-로그-시스템">기본 로그 시스템</h3>
<p>Microsoft의 기본 로깅 메소드를 사용 시 appsetting.json에 다음처럼 작성한다.</p>
<pre><code>&quot;Logging&quot;: {
  &quot;LogLevel&quot;: {
    &quot;Default&quot;: &quot;Information&quot;,
    &quot;Microsoft.AspNetCore&quot;: &quot;Warning&quot;,
    &quot;Microsoft.EntityFrameworkCore.Database.Command&quot;: &quot;Error&quot;
  }
},</code></pre><h3 id="서드파티-로그-시스템">서드파티 로그 시스템</h3>
<p>Serilog와 같은 서드파티 로깅 메소드를 사용해도 기본 로깅 메소드를 오버라이드하는 형식이기 때문에 appsetting.json에 정책을 작성해 사용할 수 있다.</p>
<pre><code>{
  &quot;Serilog&quot;: {
    &quot;MinimumLevel&quot;: {
      &quot;Default&quot;: &quot;Information&quot;,
      &quot;Override&quot;: {
        &quot;Microsoft&quot;: &quot;Warning&quot;,
        &quot;Microsoft.EntityFrameworkCore.Database.Command&quot;: &quot;Error&quot;
      }
    },
    &quot;WriteTo&quot;: [
      { &quot;Name&quot;: &quot;Console&quot; }
    ]
  }
}</code></pre><p>이후 Program.cs에 다음과 같이 세팅을 읽게 해준다.</p>
<pre><code>Log.Logger = new LoggerConfiguration()
    .ReadFrom.Configuration(builder.Configuration)
    .Enrich.FromLogContext()
    .CreateLogger();

builder.Host.UseSerilog();</code></pre><hr>
<h2 id="2-swagger에-주석-달기">2. Swagger에 주석 달기</h2>
<ol>
<li><p>다음과 같이 API 설명서 파일 생성
<img src="https://velog.velcdn.com/images/_hoya_/post/9545604c-b0d1-4232-865d-56c369dbc036/image.png" alt=""></p>
</li>
<li><p>다음 코드 추가</p>
<pre><code>builder.Services.AddSwaggerGen(options =&gt;
{
 // using System.Reflection;
 var xmlFilename = $&quot;{Assembly.GetExecutingAssembly().GetName().Name}.xml&quot;;

 options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});</code></pre></li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[리눅스 명령어 모음]]></title>
            <link>https://velog.io/@_hoya_/LinuxCommands</link>
            <guid>https://velog.io/@_hoya_/LinuxCommands</guid>
            <pubDate>Sun, 22 Jun 2025 14:48:49 GMT</pubDate>
            <description><![CDATA[<p>리눅스에서 사용하는 각종 단축키들을 정리해둔 문서
bashrc를 기준으로 작성하였다.</p>
<hr>
<h1 id="1-파일-및-디렉토리-관리">1. 파일 및 디렉토리 관리</h1>
<h2 id="파일의-종류">파일의 종류</h2>
<ul>
<li>일반 파일</li>
<li>디렉터리 (윈도우의 폴더)</li>
<li>심볼릭 링크 (윈도우의 바로가기)</li>
<li>장치 파일 (키보드, 하드디스크 등도 파일로 취급)</li>
</ul>
<h2 id="-루트">/ 루트</h2>
<ul>
<li><code>dev</code> : 장치 파일</li>
<li><code>home</code> : 사용자 홈 디렉토리</li>
<li><code>root</code> : root 계정의 홈 디렉토리</li>
<li><code>tmp</code> : 임시 파일</li>
<li><code>usr</code> : 기본 실행 파일, 라이브러리 등</li>
</ul>
<h2 id="파일-명령어">파일 명령어</h2>
<h3 id="디렉토리-위치">디렉토리 위치</h3>
<ul>
<li><code>pwd</code> : 현재 작업중 위치</li>
<li><code>cd</code> : 지정한 디렉토리로 이동<ul>
<li>절대 경로 (/로 시작을 한다, 절대 경로는 유일함)</li>
<li>상대 경로 (/이외의 문자로 시작, 현재 디렉토리를 기준)</li>
<li><code>.</code> : 현재 디렉토리</li>
<li><code>..</code> : 상위 디렉토리(뒤로가기)</li>
</ul>
</li>
</ul>
<h3 id="디렉토리-내용">디렉토리 내용</h3>
<ul>
<li><code>ls</code> : 디렉토리의 내용 출력<ul>
<li><code>-a</code> : 숨김 파일 포함(all)</li>
<li><code>-l</code> : 파일의 상세 정보 출력</li>
<li><code>-F</code> : 파일의 종류 표시 (* 실행 / 디렉 @ 심볼릭)</li>
<li><code>-R</code> : 하위 디렉터리 목록 표시</li>
</ul>
</li>
</ul>
<h3 id="디렉토리-생성-삭제">디렉토리 생성, 삭제</h3>
<ul>
<li><code>mkdir</code> [절대경로/상대경로] : 디렉터리 생성<ul>
<li><code>-p</code> : 중간에 없는 디렉토리가 있으면 자동 생성</li>
</ul>
</li>
<li><code>rmdir</code> [절대경로/상대경로] : 디렉터리 삭제<ul>
<li><code>-p</code> : 중간에</li>
</ul>
</li>
</ul>
<h3 id="내용-출력">내용 출력</h3>
<ul>
<li><code>cat</code> : 파일 내용 전부 출력</li>
<li><code>more</code> : 파일 내용을 일부 출력 (space : 다음화면  enter : 다음행  q : 종료)</li>
<li><code>less</code> : 파일 내용을 일부 출력 (스크롤로 이동 등)</li>
</ul>
<h3 id="파일-복사-이동-삭제">파일 복사, 이동, 삭제</h3>
<ul>
<li><code>cp [src] [dest]</code> : 파일 복사<ul>
<li><code>-r</code> : 디렉토리 복사</li>
</ul>
</li>
<li><code>mv [src] [dest]</code> : 파일 이동</li>
<li><code>rm</code> : 파일 삭제(복구 불가)<ul>
<li><code>-r</code> : 디렉토리까지 통으로 삭제</li>
</ul>
</li>
</ul>
<h3 id="바로가기">바로가기</h3>
<ul>
<li><code>ln</code> : 바로가기 만들기(하드 링크, 어떤 파일의 다른 이름)<ul>
<li><code>-s</code> : 심볼릭 링크(윈도우의 바로가기 느낌)</li>
</ul>
</li>
</ul>
<h3 id="파일-내용-검색">파일 내용 검색</h3>
<ul>
<li><code>grep [옵션] [패턴] [파일]</code> : 파일 내용 검색<ul>
<li><code>-n</code> 행번호 출력</li>
<li><code>-i</code> 대소문자 구분 없이</li>
</ul>
</li>
</ul>
<h3 id="파일-검색">파일 검색</h3>
<ul>
<li><code>find [경로] [조건] [동작]</code> : 파일 검색<ul>
<li><code>name [파일이름]</code></li>
<li><code>type [파일종류]</code></li>
<li><code>user [유저]</code></li>
</ul>
</li>
</ul>
<hr>
<h1 id="2-vi-에디터">2. vi 에디터</h1>
<h2 id="에디터-열기">에디터 열기</h2>
<ul>
<li><code>vi</code><ul>
<li>빈파일 열림(새 문서)</li>
</ul>
</li>
<li><code>vi [파일명]</code><ul>
<li>존재하는 파일이면 파일 열림</li>
<li>존재하지 않으면 새 문서</li>
</ul>
</li>
</ul>
<h2 id="모드">모드</h2>
<ul>
<li>vi 편집기의 모드는 입력 모드와 명령 모드로 나뉨<ul>
<li>명령모드 -&gt; 입력모드 : <code>i</code></li>
<li>입력모드 -&gt; 명령모드 : esc키</li>
</ul>
</li>
<li>파일 저장/종료 : q(quit) i(강제) w(write)<ul>
<li><code>q</code> : 그냥 종료</li>
<li><code>q!</code> : 강제 종료 (고친 사항이 있어도 강제종료)</li>
<li><code>w [파일명]</code> : 파일 저장</li>
<li><code>wq</code> : 파일 저장하고 종료</li>
</ul>
</li>
<li>명령 모드 -&gt; 입력 모드<ul>
<li><code>i</code> : 현재 커서 위치</li>
<li><code>a</code> : 커서 뒤</li>
<li><code>o</code> : 커서 기준 다음행</li>
<li><code>I</code> : 커서 기준 행의 시작</li>
<li><code>A</code> : 커서 기준 행의 끝</li>
<li><code>O</code> : 커서 기준 이전 행</li>
</ul>
</li>
</ul>
<h2 id="화면-이동">화면 이동</h2>
<ul>
<li>초기 vi는 상하좌우(kjhl)</li>
<li>통상적인 키로도 매핑 되어있음<ul>
<li>상하좌우 방향키</li>
<li>home, end : 행의 앞/끝 이동</li>
<li>pgup, pgdw : 화면 위/아래</li>
</ul>
</li>
<li>특정 행 처리<ul>
<li><code>G</code> or <code>:$</code> : 파일의 마지막 행으로 이동</li>
<li><code>행번호 + G</code> or <code>:행번호</code> : 특정 행 번호 이동</li>
<li><code>set nu[mber]</code> : 행 번호 표시</li>
<li><code>set nonu</code> : 행 번호 숨김</li>
</ul>
</li>
</ul>
<h2 id="수정">수정</h2>
<ul>
<li><code>r</code> : 커서의 글자를 다른 글자로 수정</li>
<li><code>s</code> : 커서의 글자 삭제 이후 ESC를 입력할 때까지 내용 입력</li>
<li><code>#cw</code> : 단어를 삭제하고 해당 위치에 입력 모드</li>
<li><code>#cc</code> : 행을 삭제하고 해당 위치에 입력 모드</li>
<li><code>C</code> : 커서 위치부터 행의 끝까지 수정</li>
</ul>
<h2 id="삭제">삭제</h2>
<ul>
<li><code>#x</code> : 커서 위치 글자 삭제</li>
<li><code>#dw</code> : 커서 위치 단어 삭제</li>
<li><code>#dd</code> : 커서 위치 행 삭제</li>
<li><code>D</code> : 커서 위치부터 행 끝까지 삭제</li>
</ul>
<h2 id="명령-취소">명령 취소</h2>
<ul>
<li><code>Ctrl + Z</code> : fg로 돌아간다</li>
<li><code>u</code> : 명령 취소</li>
<li><code>U</code> : 행에서 한 모든 명령 취소<ul>
<li><code>:e!</code> : 마지막 저장 내용 이후의 모든 변경 내용 취소</li>
</ul>
</li>
</ul>
<h2 id="복사붙여넣기">복사/붙여넣기</h2>
<ul>
<li><code>#yy</code> : 커서 위치 행 복사</li>
<li><code>p</code> : 커서 위치한 행 아래에 붙임</li>
<li><code>P</code> : 커서 위치한 행 위에 붙임</li>
<li><code>cc</code> : dd 상태에서 p 잘라 붙이기</li>
</ul>
<h2 id="검색">검색</h2>
<ul>
<li><code>/[단어]</code> : 단어 검색(아래 방향으로)</li>
<li><code>?[단어]</code> : 단어 검색(위 방향으로)</li>
<li><code>n</code> : 다음 단어 검색</li>
<li><code>N</code> : 이전 단어 검색</li>
</ul>
<h2 id="바꾸기">바꾸기</h2>
<ul>
<li><p><code>:[범위]s/[단어1]/[단어2]</code> : 범위 내 첫 번째 단어1-&gt;단어2</p>
</li>
<li><p><code>:[범위]s/[단어1]/[단어2]/g</code> : 범위 내 모든 단어1-&gt;단어2</p>
</li>
<li><p><code>:s/[단어1]/[단어2]</code> : 커서 행에서 나오는 첫 번째 단어1-&gt;단어2</p>
</li>
<li><p><code>:s/[단어1]/[단어2]/g</code> : 커서 행에서 나오는 모든 단어1-&gt;단어2</p>
</li>
<li><p><code>:%s/[단어1]/[단어2]/g</code> : 파일 전체의 단어1-&gt;단어2</p>
</li>
<li><p><code>:5,10s/[단어1]/[단어2]/g</code> : 5~10줄 단어1-&gt;단어2</p>
</li>
</ul>
<h2 id="기타">기타</h2>
<ul>
<li><code>.</code> : 이전 명령 번복</li>
<li><code>~</code> : 커서 위치의 글자 대/소문자 변경</li>
</ul>
<hr>
<h1 id="3-쉘">3. 쉘</h1>
<ul>
<li>사용자가 리눅스 커널과 대화(명령)하기 위한 도구</li>
<li>shell의 종류<ul>
<li><code>/bin/sh</code> : 본쉘(구형쉘)</li>
<li><code>/bin/bash</code> : 베시쉘, 주로 사용</li>
</ul>
</li>
</ul>
<h2 id="출력">출력</h2>
<ul>
<li><code>echo [문자열]</code></li>
</ul>
<h2 id="특수문자">특수문자</h2>
<h3 id="문자열-관련-특수문자">문자열 관련 특수문자</h3>
<ul>
<li><code>*</code> : 모든 길이의 아무 문자열로 대체 가능</li>
<li><code>?</code> : 하나의 문자</li>
<li><code>[]</code> : 괄호 안의 하나의 문자만</li>
</ul>
<h3 id="특수문자-취소">특수문자 취소</h3>
<ul>
<li><code>&#39; &#39;</code> : 문자열 안의 특수문자 모두 없앰</li>
<li><code>&quot; &quot;</code> : 문자열 안의 특수문자 모두 없앰($ &#39; \ 제외)</li>
<li><code>\</code> : 특수문자 효과를 없애고 일반문자 처리</li>
</ul>
<h3 id="경로-관련-특수문자">경로 관련 특수문자</h3>
<ul>
<li><code>~</code> : (틸트) 작업자의 홈 디렉토리</li>
<li><code>-</code> : (하이픈) 이전 작업 디렉토리</li>
</ul>
<h3 id="명령-관련-특수문자">명령 관련 특수문자</h3>
<ul>
<li><code>;</code> : (세미콜론) 왼쪽 명령부터 차례로 실행</li>
<li><code>|</code> : (파이프) 왼쪽 명령의 결과를 오른쪽으로 전달</li>
</ul>
<h2 id="입출력-redirection">입출력 redirection</h2>
<p>결과를 모니터 출력이 아니라 파일로 저장하고 싶다면?</p>
<p>리눅스는 키보드, 모니터 장치도 파일로 관리를 한다.
파일 구분을 위해 일련번호가 붙는데, 이를 파일 디스크립터라고 한다
표준 입출력 장치</p>
<ul>
<li>0 : stdin 표준 입력</li>
<li>1 : stdout 표준 출력</li>
<li>2 : stderr 표준 오류</li>
</ul>
<h3 id="출력-리디렉션">출력 리디렉션</h3>
<p><code>&gt;</code> : 표준 출력 파일을 바꿈 (redirection)</p>
<ul>
<li><code>1&gt; [파일 이름]</code> : 해당 파일 이름에 출력 결과 저장</li>
<li><code>2&gt; [파일 이름]</code> : 해당 파일 이름에 발생 오류 저장</li>
<li>출력인지 오류인지 모를 때에는 둘 다 redirection <code>1&gt; [파일 이름1] 2&gt; [파일 이름2]</code></li>
</ul>
<p><code>2&gt; dev/null</code> : 특수 파일(버려지는 곳)으로 리디렉션하여 오류 무시</p>
<p><code>1&gt; [파일 이름] 2&gt;&amp;1</code> : <code>2&gt;&amp;1</code>을 이용해 error를 out으로 리디렉션</p>
<h3 id="입력-리디렉션">입력 리디렉션</h3>
<p><code>&lt;</code> : 파일 내용을 표준 입력 장치로</p>
<h2 id="쉘-환경-변수">쉘 환경 변수</h2>
<ul>
<li><code>env</code> 환경 변수 목록 보기</li>
<li><code>alias [별칭]=&#39;[명령어]&#39;</code> : 명령어에 별칭 붙이기</li>
<li><code>HISTORY</code> : 명령 히스토리 목록을 보여줌<ul>
<li><code>![번호]</code> : 해당 번호의 명령어를 재실행</li>
<li><code>![문자열]</code> : 히스토리에서 해당 문자열로 시작하는 마지막 명령 재실행</li>
</ul>
</li>
</ul>
<hr>
<h1 id="4-파일-권한">4. 파일 권한</h1>
<h2 id="파일-권한-확인">파일 권한 확인</h2>
<p><code>ls -l</code>로 파일 권환과 관련된 부분을 확인할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/13f51fb1-b20f-4246-96d3-fac96565e0a1/image.png" alt=""></p>
<p>앞에서부터 다음과 같은 정보를 가진다.</p>
<ul>
<li><code>-</code> 파일의 종류 (-일반파일 d디렉토리)</li>
<li><code>rw-rw-r--.</code> : 파일 권한</li>
<li><code>1</code> : 하드 링크 개수 (동일한 파일, 여러 개의 이름)</li>
<li><code>user1</code> : 파일 소유자</li>
<li><code>user1</code> : 파일이 속한 그룹</li>
<li><code>93</code> : 파일의 크기</li>
<li><code>10월 1 22:49</code> : 마지막 수정 날짜</li>
<li><code>err.txt</code>.: 파일</li>
</ul>
<p><code>groups [이름]</code> : 모든 계정은 그룹을 가진다, 그룹별로 파일 권한을 부여할 수 있다</p>
<h2 id="파일-권한을-읽는-법">파일 권한을 읽는 법</h2>
<pre><code>소유자   그룹    나머지(소유자도 그룹도 아닌 계정)
rw-     rw-     r--</code></pre><ul>
<li><code>r</code> : 읽기 권한</li>
<li><code>w</code> : 쓰기 권한</li>
<li><code>x</code> : 실행 권한</li>
<li><code>-</code> : 권한 없음</li>
</ul>
<h2 id="접근-권한-변경">접근 권한 변경</h2>
<p>파일의 소유자 / root(관리자) 계정만 변경 가능</p>
<p><code>chmod</code> : 접근 권한 변경 명령어</p>
<ul>
<li>기호 방식과 숫자 방식으로 변경 가능
<img src="https://velog.velcdn.com/images/_hoya_/post/9ad57a1b-08ec-4235-8b84-a22d19925447/image.png" alt=""></li>
</ul>
<h2 id="특수-접근-권한-설정">특수 접근 권한 설정</h2>
<p>위에서 설명한 일반적인 접근 권한 외에 다음과 같은 특수 권한을 설정할 수 있다.</p>
<ul>
<li><code>SetUID</code> : (4) 실행 도중에는 실행한 사용자가 아닌 파일 소유자의 권한 적용</li>
<li><code>SetGID</code> : (2) 실행 도중에는 실행한 사용자가 아닌 파일 그룹 권한 적용</li>
<li><code>StickyBit</code> : (1)<ul>
<li>디렉터리 전용</li>
<li>해당 디렉터리는 누구나 파일 생성 가능</li>
<li>파일은 파일 생성자의 소유</li>
<li>다른 사용자는 파일 삭제 불가</li>
</ul>
</li>
</ul>
<hr>
<h1 id="5-프로세스">5. 프로세스</h1>
<p>프로세스는 실행중인 프로그램을 뜻한다.</p>
<h2 id="리눅스-프로세스의-특징">리눅스 프로세스의 특징</h2>
<ol>
<li>PID 고유 번호가 있음<ul>
<li>systemd (1)</li>
<li>kthreadd (2)</li>
</ul>
</li>
<li>부모-자식 관계 <code>ex)bash shell - vi</code><ul>
<li>통상적으로 자식이 종료할 때 부모에게 종료 정보를 보냄</li>
<li>부모가 종료 정보를 받아서 프로세스 테이블(실행 목록)에서 자식 제거</li>
<li>즉 부모가 자식의 탄생/소멸을 모두 관리함</li>
</ul>
</li>
<li>예외가 발생할 수 있음<ul>
<li>자식이 종료하기 전에 부모가 먼저 종료할 시<ul>
<li>자식 프로세스는 고아 프로세스가 된다.</li>
<li>1번 프로세스에게 입양된 후 새 부모를 갖게 된다</li>
</ul>
</li>
<li>자식이 종료했는데 부모가 특정 이유로 자식 제거 처리를 안할 시<ul>
<li>자식 프로세스는 좀비 프로세스가 되어 리소스를 차지한다</li>
<li>좀비 프로세스가 누적되면 리소스 낭비도 누적된다</li>
</ul>
</li>
</ul>
</li>
</ol>
<h2 id="프로세스-목록-확인">프로세스 목록 확인</h2>
<p><code>ps</code></p>
<ul>
<li>플래그 없음 : 현재 셸/터미널에서 실행한 사용자의 프로세스</li>
<li><code>-e</code> : 실행중인 모든 프로세스 정보 출력</li>
<li><code>-f</code> : 자세한 정보 출력</li>
<li><code>-u</code> : 지정한 사용자에 대한 모든 프로세스 출력</li>
<li><code>-p</code> : 지정한 pid의 프로세스 정보 출력</li>
</ul>
<h3 id="프로세스-확인-방법">프로세스 확인 방법</h3>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/5b42f343-6698-40b1-9c77-a6f2884b9431/image.png" alt=""></p>
<ul>
<li>UID : 유저ID</li>
<li>PID : 프로세스 아이디</li>
<li>PPID : 부모 프로세스 아이디</li>
<li>C : CPU 사용량</li>
<li>STIME : 프로세스 시작 시간</li>
<li>TTY : 프로세스가 실행된 터미널 종류/번호</li>
<li>TIME : 프로세스 실행 시간</li>
<li>CMD : 프로그램 이름</li>
</ul>
<h2 id="프로세스-종료">프로세스 종료</h2>
<p>응답이 없거나 좀비가 된 프로세스를 종료하는 방법</p>
<ol>
<li>PID를 확인한다</li>
<li><code>kill</code> 혹은 <code>pkill</code>을 사용한다</li>
</ol>
<p>이 때 강제종료가 아닌 해당 프로세스에 종료 시그널을 보내는 형식이다.</p>
<p>시그널 신호는 <code>kill -l</code>을 이용해 확인 가능
<img src="https://velog.velcdn.com/images/_hoya_/post/1a61e515-36c5-4446-a538-da78830850c4/image.png" alt=""></p>
<ul>
<li><code>SIGHUP(1)</code> : 터미널과 연결이 끊어졌을 때</li>
<li><code>SIGINT(2)</code> : Ctrl + C로 사용자가 강제 종료했을 때</li>
<li><code>SIGKILL(9)</code> : 강제 종료</li>
</ul>
<h3 id="프로세스-종료-방법">프로세스 종료 방법</h3>
<p>kill을 이용하는 방법</p>
<ul>
<li><code>kill [-시그널번호(옵션] [PID]</code> </li>
<li><code>pkill [프로세스 이름]</code></li>
</ul>
<p>프로세스 관리창을 이용하는 방법</p>
<ul>
<li><code>top</code>명령어를 이용해 프로세스 관리창(윈도우의 작업 관리자)에 진입할 수 있다.<ul>
<li><code>h</code> : 도움말</li>
<li><code>k</code> : 프로세스 종료</li>
<li><code>u</code> : 특정 유저</li>
<li><code>M</code> : 사용 메모리에 따라 정렬</li>
<li><code>P</code> : CPU 사용량에 따라 정렬</li>
</ul>
</li>
</ul>
<h2 id="백그라운드-프로세스">백그라운드 프로세스</h2>
<p>기본적으로 사용자가 수행하는 작업은 foreground 작업</p>
<ul>
<li><code>Ctrl+C</code>로 포그라운드 작업 종료 가능</li>
</ul>
<p>대기하는 동안 다른 작업을 시키고 싶으면</p>
<ul>
<li>맨 뒤에 <code>&amp;</code>를 붙이면 백그라운드에서 실행됨</li>
</ul>
<p>다만 백그라운드에서 입출력 작업도 같이 수행시 입력창이 뒤섞일 수 있는 문제가 있다.</p>
<ul>
<li><code>(sleep 2; echo 일어님)&amp;</code> 수행 시 입력창에 &#39;일어남&#39;이 적혀있게 된다</li>
<li>이런 문제를 해결하기 위해 리디렉션으로 입출력 경로를 이용해 지정하는 것이 좋다.</li>
</ul>
<h3 id="백그라운드-작업-보기-및-전환">백그라운드 작업 보기 및 전환</h3>
<ul>
<li>백그라운드 작업은 <code>jobs</code>로 확인 가능</li>
<li><code>fg [%작업번호]</code>로 백그라운드 작업을 포그라운드로 전환 가능</li>
<li><code>bg [%작업번호]</code>로 포그라운드 작업을 백그라운드로 전환 가능</li>
</ul>
<h2 id="작업-예약">작업 예약</h2>
<h3 id="일회성-예약">일회성 예약</h3>
<p><code>at [옵션] [시간]</code> 명령어로 특정 시간에 작업이 수행되게 예약할 수 있다.</p>
<ul>
<li><code>l</code> : 예약된 작업 확인</li>
<li><code>d</code> : 예약된 작업 삭제</li>
</ul>
<p><code>at</code>으로 예약할 작업들을 다 설정했으면 <code>ctrl + d</code>로 예약 작업을 종료한다.</p>
<h3 id="반복성-예약">반복성 예약</h3>
<p><code>crontab [-u 사용자][옵션][파일명]</code>로 특정 시간마다 파일이 실행되게 할 수 있다.</p>
<ul>
<li><code>e</code> : 사용자의 crontab 파일 편집</li>
<li><code>l</code> : 파일 목록 출력</li>
<li><code>r</code> : 파일 삭제</li>
</ul>
<p><code>crontab</code> 폴더에는 다음과 같은 규격을 통해 작업을 예약할 수 있다.</p>
<pre><code>분(0~59) 시(0~23) 일(1~31) 월(1~12) 요일(0~6) 작업내용

// 매 54분마다 echo hello를 실행한다
54       *        *       *        *        echo hello
</code></pre><h1 id="6-사용자-계정">6. 사용자 계정</h1>
<h2 id="사용자-계정의-중요-파일">사용자 계정의 중요 파일</h2>
<h3 id="사용자-계정-정보">사용자 계정 정보</h3>
<p><code>etc/passwd</code> : 사용자 계정 정보 확인 가능
<img src="https://velog.velcdn.com/images/_hoya_/post/b5f4cd65-3048-4126-98bf-084286afa2cd/image.png" alt=""></p>
<ul>
<li><code>hoya</code> : 로그인 id(사용자 계정)</li>
<li><code>x</code> : 초창기 유닉스에서는 암호 표시, 보안 때문에 /etc/shadow로 암호 이동</li>
<li><code>1000</code> : UID(사용자 id), 0~999는 시스템 예약, 사용자 계정은 1000부터 시작</li>
<li><code>1000</code> : GID(그룹 id), 지정하지 않으면 로그인 ID, /etc/group 파일 정보</li>
<li><code>,</code> : 유저에 대한 설명, 코멘트 등</li>
<li><code>/home/hoya</code> : 홈 디렉토리(로그인 시 홈 디렉토리 시작)</li>
<li><code>/bin/bash</code> : 로그인 셸(로그인 시 기본적으로 사용하는 셸)</li>
</ul>
<h3 id="사용자-비밀번호-정보">사용자 비밀번호 정보</h3>
<p><code>etc/shaodw</code> : 사용자 비밀번호 정보 확인 가능
다만 root계정으로만 확인 가능</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/13c6aaa1-1c58-4136-ac1b-16b572198472/image.png" alt=""></p>
<p>앞에서부터 다음과 같은 정보를 지니고 있음</p>
<ul>
<li>로그인ID</li>
<li>비밀번호 해쉬값</li>
<li>암호 최종일 변경</li>
<li>암호 변경 후 사용해야 하는 최소 기간</li>
<li>암호 사용 최대 기간</li>
<li>암호 만료 전 경고를 시작하는 날</li>
<li>암호가 만료된 후에도 로그인이 가능할 일수</li>
<li>사용자 계정이 만료되는 날</li>
<li>추후 확장 플래그</li>
</ul>
<h3 id="사용자-계정-기본-설정">사용자 계정 기본 설정</h3>
<p><code>etc/login.defs</code> : 파일에 사용자가 만들어질 때의 기본 환경감 확인 가능</p>
<h3 id="그룹-정보">그룹 정보</h3>
<p><code>etc/group</code> : 그룹 정보 확인 가능
<img src="https://velog.velcdn.com/images/_hoya_/post/292f50ef-f936-416b-9888-1c3d02f3a106/image.png" alt=""></p>
<ul>
<li><code>hoya</code> : 그룹 이름</li>
<li><code>x</code> : 암호 (etc/gshadow로 이전됨)</li>
<li><code>2000</code> : GID 그룹 식별 번호</li>
<li>그룹 멤버 : 그룹에 속한 멤버목록</li>
</ul>
<p><code>etc/gshadow</code> : 그룹 비밀번호 확인 가능
<img src="https://velog.velcdn.com/images/_hoya_/post/553e17b5-10b7-4e22-be0e-71b0f1c3fc62/image.png" alt=""></p>
<ul>
<li><code>hoya</code> : 그룹 이름</li>
<li><code>!</code> : 그룹의 비밀번호</li>
<li>? : 그룹의 관리자</li>
<li>? : 그룹에 속한 멤버</li>
</ul>
<h2 id="사용자-계정-관리">사용자 계정 관리</h2>
<h3 id="사용자-추가">사용자 추가</h3>
<p><code>useradd [옵션] [로그인ID]</code> : 유저 추가</p>
<ul>
<li><code>u</code> : uid</li>
<li><code>o</code> : uid 중복 허용(거의 사용하지 않음)</li>
<li><code>g</code> : gid 기본 그룹</li>
<li><code>s</code> : 기본 쉘</li>
<li><code>c</code> : tjfaud</li>
<li><code>e</code> : 유효기간</li>
<li><code>D</code> : 기본값 설정 확인</li>
</ul>
<h3 id="사용자-비밀번호-설정">사용자 비밀번호 설정</h3>
<p><code>passwd [사용자id]</code> : 해당 사용자의 비밀번호 설정</p>
<h3 id="사용자-계정정보-수정">사용자 계정정보 수정</h3>
<p><code>usermod [옵션] [로그인ID]</code> : 유저정보 수정</p>
<ul>
<li>옵션은 사용자 추가와 거의 유사</li>
<li><code>l [새id] [기존id]</code> : 계정 이름 바꾸기</li>
</ul>
<h3 id="사용자-계정-삭제">사용자 계정 삭제</h3>
<p><code>userdel [옵션] [로그인id]</code> : 사용자 계정 삭제</p>
<ul>
<li><code>r</code> : 홈 디렉토리도 삭제</li>
<li><code>f</code> : 사용자가 로그인 중이어도 강제 삭제</li>
</ul>
<h2 id="그룹-관리">그룹 관리</h2>
<h3 id="그룹-추가">그룹 추가</h3>
<p><code>groupadd [옵션] [그룹명]</code></p>
<ul>
<li><code>g</code> : gid 설정</li>
<li><code>o</code> : 그룹명 중복 허용</li>
</ul>
<h3 id="그룹-설정-변경">그룹 설정 변경</h3>
<p><code>groupmod [옵션] [그룹명]</code></p>
<ul>
<li><code>g</code> : gid 설정</li>
<li><code>o</code> : 그룹명 중복 허용</li>
<li><code>n [이름]</code> : name 그룹명을 다른 이름으로 교체</li>
</ul>
<h3 id="그룹-삭제">그룹 삭제</h3>
<p><code>groupdel [그룹명]</code></p>
<h3 id="그룹-설정">그룹 설정</h3>
<p><code>gpasswd [옵션] [그룹명]</code></p>
<ul>
<li><code>a [사용자]</code> : 사용자 계정을 그룹에 추가</li>
<li><code>d [사용자]</code> : 사용자 계정을 그룹에서 제거</li>
<li><code>r</code> : 그룹 암호 제거</li>
</ul>
<h3 id="소속-그룹-확인-및-변경">소속 그룹 확인 및 변경</h3>
<ul>
<li><code>id</code> : 소속 그룹 확인</li>
<li><code>newgrp</code> : 소속 그룹 변경</li>
</ul>
<h2 id="사용자-정보-관리">사용자 정보 관리</h2>
<p><code>who</code> : 시스템에 로그인한 사용자 목록</p>
<ul>
<li><code>q</code> : 사용자 이름만 출력</li>
<li><code>H</code> : 헤더도 출력</li>
<li><code>r</code> : 현재 실행 레벨 출력</li>
</ul>
<p><code>w [사용자명]</code> : 사용자가 현재 무엇을 하고있는지 확인 가능</p>
<p><code>last</code> : pc 접속 히스토리를 확인</p>
<h2 id="root-권한-빌려오기">root 권한 빌려오기</h2>
<p><code>sudo</code>를 이용해 루트 권한을 빌려올 수 있다.
이 때 루트 권한을 사용하기 위해서 루트 계정이 허락을 해야 한다.</p>
<ul>
<li><code>/etc/sudoers</code> 파일에 다음과 같이 허락을 한다.<pre><code>// user2가 localhost로 접속했을 때 useradd를 사용할 수 있게 허가
user2 localhost=sbin/useradd</code></pre></li>
</ul>
<h2 id="파일-및-디렉터리-소유자그룹-변경">파일 및 디렉터리 소유자/그룹 변경</h2>
<ul>
<li><code>chown [옵션] [사용자] [대상 파일]</code> : 소유자 변경</li>
<li><code>chgrp</code> : 변경된 소유자가 속한 그룹으로 변경</li>
</ul>
<hr>
<h1 id="6-패키지">6. 패키지</h1>
<h2 id="패키지의-종류">패키지의 종류</h2>
<h3 id="rpm">RPM</h3>
<p>rpm (Redhat Package Manager)</p>
<ul>
<li>장점 : 바이너리 형태(컴파일 x), 간편 사용</li>
<li>단점 : 의존성 처리를 직접 해주지 않음, 직접 패키지를 다운로드해 설치해야함</li>
</ul>
<p>rpm의 버전체크는 <code>--version</code>으로</p>
<p><code>rpm</code> 명령어로 패키지를 설치</p>
<ul>
<li><code>-i</code> : 패키지 설치</li>
<li><code>-U</code> : 새로운 패키지는 설치, 기존 패키지는 업그레이드</li>
<li><code>-h</code> : #를 이용해 설치 진척도 표시</li>
<li><code>-v</code> : 자세한 사항 표시</li>
</ul>
<p><code>rpm -q</code>로 설치된 패키지 목록 출력</p>
<ul>
<li><code>a</code> : 전체 패키지 목록 출력</li>
<li><code>f [절대경로]</code> : 파일명을 포함하고 있는 패키지</li>
<li><code>p</code> : 패키지 지정</li>
<li><code>R</code> : 의존성 있는 패키지 목록 추천</li>
<li><code>l</code> : 패키지 내 파일 목록</li>
</ul>
<p><code>rpm -e</code>를 이용해 패킺지 삭제</p>
<h3 id="dnf">dnf</h3>
<p>CentOS에서 사용하는 것으로 yum의 업그레이드 버전
의존성을 자동으로 해결해주고, 설치 가능한 패키지 저장소도 이미 알고 있음</p>
<p><code>dnf [옵션] [명령] [패키지명]</code></p>
<ul>
<li><code>install [패키지명]</code></li>
<li><code>upgrade [패키지명]</code> : 명칭 없을시 전채 패키지 업데이트</li>
<li><code>check-update</code></li>
<li><code>search</code></li>
<li><code>remove</code></li>
<li><code>list</code></li>
<li><code>info</code></li>
</ul>
<h2 id="파일-압축">파일 압축</h2>
<p>`tar [옵션] [아카이브 파일] [파일명]</p>
<ul>
<li><code>c</code> : 새로운 tar 파일 만들기</li>
<li><code>t</code> : tar 파일 보기</li>
<li><code>x</code> : 압축 풀기</li>
<li><code>v</code> : 상세 정보</li>
<li><code>f</code> : 결과 파일의 이름 지정</li>
<li><code>z</code> : .gz로 압축</li>
<li><code>j</code> : .bz2으로 압축</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[MSSQL DB 초기화 쿼리]]></title>
            <link>https://velog.io/@_hoya_/MSSQLInitQuery</link>
            <guid>https://velog.io/@_hoya_/MSSQLInitQuery</guid>
            <pubDate>Thu, 15 May 2025 07:50:00 GMT</pubDate>
            <description><![CDATA[<p>서버 개발 도중 잦은 DB 초기화를 한 번에 편하게 하기 위해 작성한 쿼리</p>
<ul>
<li>DB 내 모든 데이터를 삭제한다.</li>
<li>DB의 Identity를 0으로 Reseed한다</li>
<li>이 때 EF Core의 Migration History를 기록하는 DB는 제외한다</li>
</ul>
<pre><code>-- 외래키 제약조건 임시로 OFF
EXEC sp_msforeachtable &quot;ALTER TABLE ? NOCHECK CONSTRAINT ALL&quot;;

-- __EFMigrationsHistory 테이블을 제외하고 모든 테이블에서 DELETE 수행
DECLARE @sql NVARCHAR(MAX) = N&#39;&#39;;
SELECT @sql += &#39;DELETE FROM [&#39; + SCHEMA_NAME(schema_id) + &#39;].[&#39; + name + &#39;];&#39; + CHAR(13)
FROM sys.tables
WHERE name &lt;&gt; &#39;__EFMigrationsHistory&#39;;

EXEC sp_executesql @sql;

-- 모든 테이블의 IDENTITY를 0으로 RESEED (__EFMigrationsHistory는 제외)
DECLARE @reseedSql NVARCHAR(MAX) = N&#39;&#39;;
SELECT @reseedSql += 
    &#39;IF EXISTS (SELECT 1 FROM sys.identity_columns WHERE object_id = OBJECT_ID(&#39;&#39;&#39; + SCHEMA_NAME(schema_id) + &#39;.&#39; + name + &#39;&#39;&#39;)) &#39; +
    &#39;DBCC CHECKIDENT (&#39;&#39;&#39; + SCHEMA_NAME(schema_id) + &#39;.&#39; + name + &#39;&#39;&#39;, RESEED, 0);&#39; + CHAR(13)
FROM sys.tables
WHERE name &lt;&gt; &#39;__EFMigrationsHistory&#39;;

EXEC sp_executesql @reseedSql;

-- 외래키 제약조건 다시 ON
EXEC sp_msforeachtable &quot;ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL&quot;;</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[윈도우에서 서버개발 환경 구축하기]]></title>
            <link>https://velog.io/@_hoya_/Build-ServerDev-Environment</link>
            <guid>https://velog.io/@_hoya_/Build-ServerDev-Environment</guid>
            <pubDate>Sat, 12 Apr 2025 13:55:11 GMT</pubDate>
            <description><![CDATA[<h1 id="개요">개요</h1>
<p>C#을 이용해 서버(게임, 웹)를 개발하기 위해 필요한 환경을 구축하는 방식에 대한 문서이다.</p>
<p>사용하는 프로그램 언어는 다음과 같다.</p>
<ul>
<li>C# (.Net 8.0)</li>
<li>MSSQL</li>
</ul>
<p>환경 및 사용 프로그램은 다음과 같다.</p>
<ul>
<li>Windows 10/11</li>
<li>Visual Studio 2022</li>
<li>Sql Server Express 2022</li>
<li>Sql Server Management Studio 20</li>
</ul>
<hr>
<h1 id="목차">목차</h1>
<h3 id="visual-studio">Visual Studio</h3>
<ul>
<li>Visual Studio 2022<ul>
<li>설치 및 설정</li>
<li>프로젝트 템플릿</li>
<li>Nuget 패키지 관리</li>
</ul>
</li>
</ul>
<h3 id="sql">SQL</h3>
<ul>
<li><p>SQL Server </p>
<ul>
<li>SQL Server 설치</li>
<li>서버 인스턴스 관리 및 삭제</li>
</ul>
</li>
<li><p>SSMS</p>
<ul>
<li>서버 인스턴스 연결 및 관리</li>
</ul>
</li>
</ul>
<h3 id="c에-db-연결하기">C#에 DB 연결하기</h3>
<ul>
<li>사전 설정</li>
<li>DB 생성/복사 및 설정</li>
<li>C# 코드에 연결</li>
</ul>
<h3 id="exe-빌드">.exe 빌드</h3>
<hr>
<h1 id="visual-studio-1">Visual Studio</h1>
<p><a href="https://learn.microsoft.com/ko-kr/visualstudio/ide/?view=vs-2022">Visual Studio 2022 설명서</a></p>
<p>Visual Studio를 사용하는 이유는 다음과 같다.</p>
<ul>
<li>C# + MSSQL에 친화적인 IDE</li>
<li>콘솔 앱, 웹앱, 게임 등 다양한 템플릿 제공</li>
<li>Nuget 패키지 관리</li>
<li>코드렌즈, 인텔리센스, Github Copilot과 같은 부가 기능</li>
</ul>
<h2 id="설치-및-설정">설치 및 설정</h2>
<ol>
<li>Visual Studio Installer 설치</li>
</ol>
<p><a href="https://visualstudio.microsoft.com/ko/">Visual Studio 홈페이지</a>에서 설치 파일을 다운받아 실행한다.</p>
<ol start="2">
<li>Visual Studio 2022 Community 설치</li>
</ol>
<p>다음과 같이 인스톨러에서 IDE를 설치한다. <img src="https://velog.velcdn.com/images/_hoya_/post/e07fe1b8-8a33-40a8-ae47-9683339fdc27/image.png" alt=""></p>
<ol start="3">
<li>각종 세팅 </li>
</ol>
<p><a href="https://learn.microsoft.com/ko-kr/visualstudio/install/modify-visual-studio?view=vs-2022">Visual Studio 워크로드</a></p>
<p>인스톨러의 설치된 IDE 쪽에 &#39;수정&#39; 버튼을 누르면 여러 개발 워크로드를 설정 및 추가할 수 있다.
각종 서버 및 게임 클라이언트 개발을 위해서 다음과 같은 워크로드를 추가한다.</p>
<ul>
<li>ASP.NET 및 웹 개발 -&gt; 웹서버 개발 워크로드</li>
<li>.NET 데스크톱 개발 -&gt; C# 콘솔 앱 개발 워크로드, C# 서버 개발에 필요</li>
<li>C++을 사용한 데스크톱 개발 -&gt; C++ 콘솔 앱 개발 워크로드, C++ 서버 개발에 필요</li>
<li>Unity를 사용한 게임 개발 -&gt; 유니티 엔진 개발 워크로드</li>
<li>C++를 사용한 게임 개발 -&gt; 언리얼 등 C++ 사용 엔진 개발 워크로드</li>
<li>데이터 스토리 및 처리 -&gt; SQL 개발 워크로드</li>
</ul>
<h2 id="프로젝트-템플릿">프로젝트 템플릿</h2>
<p>Visual Studio의 새 프로젝트 만들기를 클릭하면 여러 프로젝트 템플릿을 사용하여 개발을 시작할 수 있다.</p>
<ul>
<li>콘솔 앱 : PC에서 실행 가능한 .NET 애플리케이션을 만든다. C# 게임 서버 템플릿</li>
<li>ASP.NET : 주로 웹서버 및 API서버를 만들 때 사용한다.</li>
<li>Blazor : 웹페이지 및 웹 애플리케이션을 만든다.</li>
</ul>
<h2 id="nuget-패키지">Nuget 패키지</h2>
<p><a href="https://learn.microsoft.com/ko-kr/nuget/what-is-nuget">Nuget이란</a></p>
<p><a href="https://learn.microsoft.com/ko-kr/nuget/consume-packages/install-use-packages-visual-studio">Nuget 패키지 관리자</a></p>
<p>Nuget 패키지 관리자를 사용해 각종 패키지를 설치 및 사용할 수 있다.</p>
<hr>
<h1 id="sql-1">SQL</h1>
<p><a href="https://www.microsoft.com/ko-kr/sql-server/sql-server-downloads">MS SQL Server 다운로드</a></p>
<p>Visual Studio에서도 MSSQL DB에 대한 접근을 할 수 있지만 DB 설정 및 데이터 직접 조작을 위해 다음과 같은 도구를 사용한다.</p>
<h2 id="sql-server">SQL Server</h2>
<h3 id="sql-server-설치">SQL Server 설치</h3>
<p>위의 MS SQL Server 다운로드 페이지에서 Express를 다운로드해 설치한다. 
그러면 다음과 같은 화면이 나오는데 원하는 옵션을 선택해 SQL Server를 설치한다.
미디어 다운로드도 클릭해 추후 서버 인스턴스 설치를 위한 미디어 파일을 받아둔다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/f0c7a1ad-8484-4083-b98e-1152e6b308b6/image.png" alt=""></p>
<p>설치 완료시 Sql Server 구성 관리자 프로그램 또한 자동으로 설치된다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/b7a1845f-c85b-4b67-bf1b-2b4854483519/image.png" alt=""></p>
<p>이를 이용해 각 DB 서버 인스턴스에 대한 설정을 관리할 수 있다.</p>
<h3 id="서버-인스턴스-설치-및-삭제">서버 인스턴스 설치 및 삭제</h3>
<p><a href="https://yegenie2.tistory.com/82">설치 방법</a>
<a href="https://learn.microsoft.com/ko-kr/sql/database-engine/install-windows/install-sql-server?view=sql-server-ver16">공식 설치 방법</a></p>
<p>새 서버 인스턴스 설치 방법은 다음과 같다.</p>
<ul>
<li>SQL Server 설치 센터 프로그램을 실행한다.</li>
<li>&#39;설치&#39; - &#39;새 SQL Server 독립 실행형 설치 또는 기존 설치에 기능 추가&#39;를 누른다</li>
<li>받아둔 미디어 파일을 실행해 설치 환경으로 들어간다.</li>
<li>설정들을 기입하고 설치를 실시한다.</li>
</ul>
<p><a href="https://learn.microsoft.com/ko-kr/sql/sql-server/install/uninstall-an-existing-instance-of-sql-server-setup?view=sql-server-ver16&amp;tabs=Windows10">공식 삭제 방법</a></p>
<p>기존 서버 인스턴스 삭제 방법은 다음과 같다.</p>
<ul>
<li>&#39;설정&#39; - &#39;앱&#39; - &#39;설치된 앱&#39;에서 Microsoft SQL Server 2022를 찾아 삭제 버튼을 누른다.</li>
<li>옵션 선택창에서 &#39;제거&#39;를 누른다.</li>
<li>제거할 SQL Server 인스턴스를 선택한 후 삭제를 실시한다.</li>
</ul>
<h3 id="서버-인스턴스-관리-문서">서버 인스턴스 관리 문서</h3>
<p><a href="https://bumday.tistory.com/224">SQL 비밀번호를 까먹었을 때</a></p>
<h2 id="ssms">SSMS</h2>
<p>각 DB 인스턴스를 간단하게 연결해 안의 데이터베이스에 접근, CRUD작업 등을 수행할 수 있는 도구이다.</p>
<p><a href="https://learn.microsoft.com/ko-kr/ssms/download-sql-server-management-studio-ssms?view=sql-server-ver16">SSMS 다운로드</a></p>
<h3 id="서버-인스턴스-연결-및-관리">서버 인스턴스 연결 및 관리</h3>
<p><a href="https://coderzero.tistory.com/entry/%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%B2%A0%EC%9D%B4%EC%8A%A4-SSMSSQL-Server-Management-Studio-%EC%82%AC%EC%9A%A9%EB%B2%95">SSMS 사용법</a></p>
<ul>
<li>개체 탐색기의 &#39;연결&#39; 버튼을 이용해 컴퓨터 내에 존재하는 SQL Server 인스턴스에 연결 가능</li>
<li>삭제된 서버 인스턴스가 캐싱되어 연결 목록에 뜨면 다음 Powershell 스크립트로 캐시를 초기화해준다.<pre><code>Remove-Item -Path &quot;$env:APPDATA\Microsoft\SQL Server Management Studio&quot; -Recurse -Force</code></pre></li>
</ul>
<hr>
<h1 id="c에-db-연결하기-1">C#에 DB 연결하기</h1>
<p>사실 EFCore를 사용하면 알아서 DB 인스턴스를 만들어서 자동으로 연결 및 관리해준다.
하지만 EFCore를 안 쓰는 예전 코드도 존재하고, 인증서 연결 및 비밀번호 설정과 같은 보안 기법은 따로 설정한 후 연결하는 것이 편하기 때문에 작성하였다.</p>
<h2 id="사전-설정">사전 설정</h2>
<h3 id="로컬-인증서-생성-및-적용">로컬 인증서 생성 및 적용</h3>
<p>인증서 관련 오류가 발목을 잡을 수 있으므로 테스트 환경이라면 그냥 윈도우 인증 쓰자.</p>
<p>원래는 PowerShell에서 개인 인증서 만들고 그걸 SQL Server 구성 관리자에서 적용해야 되지만
ChatGPT님께서 원터치 Powershell 스크립트를 작성해 주었다.</p>
<pre><code># -----------------------------
# 1. 변수 설정
# -----------------------------
$dnsName = &quot;YOUR-SERVER-NAME&quot;   # 서버 이름 (예: HOYA-PC 또는 FQDN)
$certName = &quot;HOYA_SQL_CERT&quot;     # 인증서 발급 이름
$storePath = &quot;cert:\LocalMachine\My&quot;
$sqlInstanceName = &quot;MSSQLSERVER&quot;  # SQL Server 인스턴스 이름 (기본 인스턴스는 MSSQLSERVER)

# -----------------------------
# 2. 기존 같은 이름 인증서 제거
# -----------------------------
$oldCerts = Get-ChildItem $storePath | Where-Object { $_.Subject -like &quot;*$certName*&quot; }
foreach ($cert in $oldCerts) {
    Remove-Item -Path &quot;$storePath\$($cert.Thumbprint)&quot; -Force
    Write-Host &quot;삭제 완료: $($cert.Subject)&quot;
}

# -----------------------------
# 3. 새 인증서 발급 (서버 인증용 확장 포함)
# -----------------------------
$cert = New-SelfSignedCertificate `
    -DnsName $dnsName `
    -CertStoreLocation $storePath `
    -Subject &quot;CN=$certName&quot; `
    -KeyLength 2048 `
    -KeyExportPolicy Exportable `
    -FriendlyName $certName `
    -NotAfter (Get-Date).AddYears(2) `
    -TextExtension @(&quot;2.5.29.37={text}1.3.6.1.5.5.7.3.1&quot;) # 서버 인증 EKU

Write-Host &quot;`n[✔] 인증서 발급 완료: $($cert.Thumbprint)`n&quot;

# -----------------------------
# 4. SQL Server에 인증서 등록
# -----------------------------
# 인증서 Thumbprint 포맷 수정 (공백 제거, 대문자)
$thumb = $cert.Thumbprint.ToUpper() -replace &quot; &quot;, &quot;&quot;

# 레지스트리에 인증서 Thumbprint 설정
$regPath = &quot;HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQLServer\SuperSocketNetLib&quot;
Set-ItemProperty -Path $regPath -Name &quot;Certificate&quot; -Value $thumb

# 암호화 활성화 (옵션)
Set-ItemProperty -Path $regPath -Name &quot;ForceEncryption&quot; -Value 1

Write-Host &quot;[✔] SQL Server에 인증서 적용 완료: $thumb`n&quot;

# -----------------------------
# 5. SQL Server 서비스 재시작
# -----------------------------
Write-Host &quot;[⏳] SQL Server 서비스를 다시 시작합니다...&quot;
Restart-Service -Name &quot;MSSQL`$$sqlInstanceName&quot; -Force -ErrorAction Stop
Write-Host &quot;[✔] SQL Server 서비스 재시작 완료!&quot;

# -----------------------------
# 완료 안내
# -----------------------------
Write-Host &quot;`n🎉 모든 작업이 완료되었습니다!&quot;
</code></pre><h3 id="db-접속용-계정-생성-및-연결">DB 접속용 계정 생성 및 연결</h3>
<p><a href="https://jione-e.tistory.com/125">DB SA 계정 생성 및 인증 절차</a>
SSMS에서 간단하게 설정할 수 있다.</p>
<h3 id="tcp-포트-오픈">TCP 포트 오픈</h3>
<p>DB를 localhost든 외부 네트워크든 넷을 통해 접속하려면 TCP 포트를 열어줘야 한다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/f2e5a840-7808-4ba7-92d0-7ca19f8ab8e0/image.png" alt=""></p>
<p>위와 같이 설정에 들어가 포트를 열어주자. DB에서 주로 사용하는 포트는 1433이다.
적용 후 서비스 재시작을 하면 포트가 열리는데 <code>telnet 127.0.0.1 1433</code>을 이용해 포트가 잘 열렸는지 확인하자.</p>
<h3 id="db-생성복사-및-설정">DB 생성/복사 및 설정</h3>
<p>DB 생성은 SSMS에서 수행하는 것이 편하다.</p>
<p>DB 복사는 파일 통복사 혹은 백업 파일 복사 및 붙여넣기 방식으로 수행할 수 있다. <a href="https://learn.microsoft.com/ko-kr/sql/relational-databases/backup-restore/quickstart-backup-restore-database?view=sql-server-ver16&amp;tabs=ssms">공식 문서</a></p>
<ol>
<li>SSMS에서 복사할 DB를 선택 후 &#39;태스크&#39; - &#39;백업&#39;으로 <code>.bak</code> 백업 파일을 만든다.</li>
<li>붙여넣을 위치의 SQL 인스턴스에 복사 마법사를 이용해 복사한다.
만약 복사가 불가능하면 접근 권한 경로 문제이므로 &#39;파일&#39;페이지에서 경로를 바꿔주면 된다.</li>
</ol>
<h3 id="c-코드에-연결">C# 코드에 연결</h3>
<p>공통적으로 connectionString을 이용해 연결한다.
IP, 포트번호, DB 인스턴스, 아이디, 비밀번호를 설정해 들어가기 위해서는 다음과 같은 커넥션 스트링을 사용한다.</p>
<pre><code>string connectionString = &quot;Server=127.0.0.1,1433;Database=YourDatabaseName;User ID=test;Password=test123;TrustServerCertificate=True;&quot;;</code></pre><p>이 때 <code>TrustServerCertificate=True</code> 는 로컬 개발용으로 SSL 인증서 오류를 무시할 수 있게 한다.
로컬 테스트가 아닌 실제 연결에서는 <code>Encrypt=True; TrustServerCertificate=False;</code>를 사용해야 한다.</p>
<h4 id="연결-스트링-작성">연결 스트링 작성</h4>
<pre><code>{
  &quot;ConnectionStrings&quot;: {
    &quot;DefaultConnection&quot;: &quot;Server=127.0.0.1,1433;Database=TestDB;User ID=sa;Password=sksckrgo1203;TrustServerCertificate=True;&quot;
  }
}</code></pre><h4 id="dbcontext-정의">DBContext 정의</h4>
<pre><code>using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public DbSet&lt;User&gt; Users { get; set; }      // Users 테이블에 매핑되는 DbSet

    private readonly IConfiguration _config;

    public AppDbContext(DbContextOptions&lt;AppDbContext&gt; options, IConfiguration config) : base(options)
    {
        _config = config;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connectionString = _config.GetConnectionString(&quot;DefaultConnection&quot;);
        optionsBuilder.UseSqlServer(connectionString);
    }
}

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
}</code></pre><h4 id="메인-함수">메인 함수</h4>
<pre><code>using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Data.SqlClient;

class Program
{
    public static void Main(string[] args)
    {
        // .NET 8의 새로운 Host 생성 방식 사용
        var builder = Host.CreateApplicationBuilder(args);

        // appsettings.json 파일을 설정에 추가 (DB 연결 문자열 등 로딩)
        builder.Configuration.AddJsonFile(&quot;appsettings.json&quot;);

        // DI 컨테이너에 DbContext 등록 (appsettings.json의 연결 문자열 자동 사용)
        builder.Services.AddDbContext&lt;AppDbContext&gt;();

        // 호스트 빌드 (구성 및 서비스가 포함된 애플리케이션 객체 생성)
        var host = builder.Build();

        // EF Core로 DB 접속
        using (var scope = host.Services.CreateScope())
        {
            // 스코프에서 DbContext 인스턴스 가져오기
            var dbContext = scope.ServiceProvider.GetRequiredService&lt;AppDbContext&gt;();
            dbContext.Database.EnsureCreated();

            // Users 테이블을 쿼리하여 이름 출력
            Console.WriteLine(&quot;EF Core로 사용자 목록:&quot;);
            foreach (var user in dbContext.Users)
            {
                Console.WriteLine($&quot;- {user.Name}&quot;);
            }
        }

        // SqlConnection을 사용하여 동일한 DB에 직접 접속
        var connectionString = builder.Configuration.GetConnectionString(&quot;DefaultConnection&quot;);

        // SqlConnection으로 DB 연결 시작
        using (var conn = new SqlConnection(connectionString))
        {
            conn.Open(); // 연결 열기

            // Users 테이블에서 첫 번째 사용자의 이름을 조회하는 SQL 명령 실행
            var cmd = new SqlCommand(&quot;SELECT TOP 1 Name FROM Users&quot;, conn);
            var result = cmd.ExecuteScalar(); // 단일 값 반환 (첫 번째 Name)

            Console.WriteLine($&quot;\nSqlConnection으로 직접 조회 결과: {result}&quot;);
        }
    }
}</code></pre><hr>
<h1 id="exe-빌드-1">.exe 빌드</h1>
<p><a href="https://cosmosproject.tistory.com/599">Visual Studio에서 .exe로 빌드 1</a>
<a href="https://youngseong.tistory.com/362">Visual Studio에서 .exe로 빌드 2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[패킷과 패킷 핸들]]></title>
            <link>https://velog.io/@_hoya_/Packet-And-PacketHandle</link>
            <guid>https://velog.io/@_hoya_/Packet-And-PacketHandle</guid>
            <pubDate>Sun, 16 Feb 2025 17:53:11 GMT</pubDate>
            <description><![CDATA[<p>프로토콜은 컴퓨터 또는 전자 기기 간의 원활한 통신을 위해 지키기로 약속한 규약이고, 패킷은 네트워크를 통해 전송되는 형식화된 데이터 덩어리이다.</p>
<p>구글은 <a href="https://protobuf.dev/">Protobuf</a> 라는 오픈소스 프로토콜을 제공하고 있다. 이를 이용해 패킷의 생성을 자동화할 수 있다.
<a href="https://usingsystem.tistory.com/152">사용방법</a></p>
<h2 id="구조">구조</h2>
<h3 id="packetmanager">PacketManager</h3>
<p>각 패킷들에 대해 수행할 작업들을 매칭해주는 매니저 클래스이다.</p>
<p><strong>스도코드</strong></p>
<pre><code>클래스 PacketManager (싱글톤)
{
    멤버 변수:
        - _instance: 싱글톤 인스턴스
        - _onRecv: 패킷 ID와 해당 패킷을 처리하는 함수 매핑 (딕셔너리)
        - _handler: 패킷 ID와 해당 핸들러 매핑 (딕셔너리)
        - CustomHandler: 사용자 정의 패킷 핸들러

    생성자:
        - PacketManager():
            - Register() 호출하여 패킷 등록

    메서드:
        - Register():
            - 각 패킷 ID를 _onRecv 및 _handler에 등록
            - 특정 패킷에 대한 처리 함수 매핑

        - OnRecvPacket(PacketSession session, ArraySegment&lt;byte&gt; buffer):
            - 버퍼에서 패킷 크기와 ID를 읽음
            - 패킷 ID에 해당하는 처리 함수(_onRecv) 실행

        - MakePacket&lt;T&gt;(PacketSession session, ArraySegment&lt;byte&gt; buffer, ushort id):
            - T 타입의 패킷 객체 생성 및 데이터 파싱
            - CustomHandler가 설정되어 있으면 실행
            - 없으면 _handler에서 패킷 ID에 맞는 핸들러를 찾아 실행

        - GetPacketHandler(ushort id):
            - 패킷 ID에 해당하는 핸들러 반환 (없으면 null 반환)
}</code></pre><h3 id="packethandler">PacketHandler</h3>
<p>각 패킷들이 수행할 작업들을 모아둔 클래스이다.
아래의 스도코드에 있는 함수들에 더해 필요한 기능들을 일일이 만들어줘야 한다.</p>
<p><strong>스도코드</strong></p>
<pre><code>클래스 PacketHandler (정적 클래스)
{
    정적 메서드:

        - C_MoveHandler(PacketSession session, IMessage packet):
            - 받은 패킷을 C_Move 타입으로 변환
            - 세션을 ClientSession 타입으로 변환
            - 플레이어 객체를 가져와 위치 이동 정보를 출력
            - 플레이어와 게임 룸이 유효한 경우, 게임 룸에 이동 요청을 추가 (Push 호출)

        - C_SkillHandler(PacketSession session, IMessage packet):
            - 받은 패킷을 C_Skill 타입으로 변환
            - 세션을 ClientSession 타입으로 변환
            - 플레이어 객체를 가져와 스킬 사용 정보를 출력
            - 플레이어와 게임 룸이 유효한 경우, 게임 룸에 스킬 사용 요청을 추가 (Push 호출)

        - C_LoginHandler(PacketSession session, IMessage packet):
            - 받은 패킷을 C_Login 타입으로 변환
            - 세션을 ClientSession 타입으로 변환
            - 로그인 처리 함수 호출 (HandleLogin)

        - C_EnterGameHandler(PacketSession session, IMessage packet):
            - 받은 패킷을 C_EnterGame 타입으로 변환
            - 세션을 ClientSession 타입으로 변환
            - 게임 입장 처리 함수 호출 (HandleEnterGame)

        - C_CreatePlayerHandler(PacketSession session, IMessage packet):
            - 받은 패킷을 C_CreatePlayer 타입으로 변환
            - 세션을 ClientSession 타입으로 변환
            - 플레이어 생성 처리 함수 호출 (HandleCreatePlayer)

        - C_EquipItemHandler(PacketSession session, IMessage packet):
            - 받은 패킷을 C_EquipItem 타입으로 변환
            - 세션을 ClientSession 타입으로 변환
            - 플레이어 객체를 가져옴
            - 플레이어와 게임 룸이 유효한 경우, 게임 룸에 아이템 장착 요청을 추가 (Push 호출)

        - C_PongHandler(PacketSession session, IMessage packet):
            - 세션을 ClientSession 타입으로 변환
            - Pong 응답 처리 함수 호출 (HandlePong)
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[네트워크 코어]]></title>
            <link>https://velog.io/@_hoya_/Network-Core</link>
            <guid>https://velog.io/@_hoya_/Network-Core</guid>
            <pubDate>Sun, 16 Feb 2025 16:44:11 GMT</pubDate>
            <description><![CDATA[<p>네트워크 코어는 TCP/IP 네트워크를 연결하고, 연결한 네트워크를 통해 주고받을 패킷의 처리 방식에 대한 코어 시스템을 구현해놓은 라이브러리이다.</p>
<p>서버뿐 아니라 클라이언트의 네트워크 연결 또한 네트워크 코어 라이브러리를 이용한다.</p>
<h2 id="구조">구조</h2>
<p>각각 다음 클래스들로 나뉘어져 있다.</p>
<ul>
<li>Listener : 서버가 사용하는 소켓 생성 및 연결 클래스</li>
<li>Connector : 클라이언트가 사용하는 소켓 생성 및 연결 클래스</li>
<li>RecvBuffer : 리스너와 커넥터가 받은 패킷들을 저장해두는 버퍼 클래스</li>
<li>Session : 패킷을 송수신받고 네트워크 상태에 따라 수행할 함수들을 모아놓은 클래스</li>
</ul>
<h3 id="listener">Listener</h3>
<p>서버에서 사용하는 소켓 연결 클래스이다. 다량의 클라이언트와 동시에 연결해야 하므로 많은 소켓을 열어둔다.
하나의 클라이언트에 연결이 성공했을 시 그 클라이언트에 대응되는 세션을 하나 만들어 할당해준다.</p>
<p><strong>스도코드</strong></p>
<pre><code>클래스 Listener
{
    멤버 변수:
        - _lock: 멀티스레드 동기화를 위한 객체
        - _listenSocket: 클라이언트 연결을 수락할 소켓
        - _sessionFactory: 세션을 생성하는 함수
        - _acceptArgs: 비동기 연결 수락을 위한 SocketAsyncEventArgs 객체

    메서드:
        - Start(IPEndPoint endPoint, Func&lt;Session&gt; sessionFactory):
            - _sessionFactory를 설정
            - _listenSocket을 생성 및 바인딩하여 대기 상태로 설정
            - RegisterAccept()를 호출하여 클라이언트 연결을 수락

        - RegisterAccept():
            - _acceptArgs를 초기화하고 비동기 Accept 시도
            - Accept가 즉시 완료되면 OnAcceptCompleted 실행

        - OnAcceptCompleted(object sender, SocketAsyncEventArgs args):
            - 연결이 성공하면:
                - 세션 객체를 생성하고 Start 호출
                - RegisterAccept()를 다시 호출하여 다음 클라이언트 대기
            - 실패 시 RegisterAccept()를 다시 호출하여 대기

        - Stop():
            - _listenSocket이 존재하면 닫고 정리
}</code></pre><h3 id="connector">Connector</h3>
<p>클라이언트에서 사용하는 소켓 연결 클래스이다. 서버 하나와만 통신하므로 하나의 소켓을 열어둔다.
서버와의 연결을 성공하면 서버 세션을 하나 만들어 할당해준다.</p>
<p><strong>스도코드</strong></p>
<pre><code>클래스 Connector
{
    멤버 변수:
        - _sessionFactory: 세션을 생성하는 함수

    메서드:
        - Connect(IPEndPoint endPoint, Func&lt;Session&gt; sessionFactory, int count = 1):
            - count 만큼 반복하여 소켓을 생성하고 연결 시도
            - RegisterConnect()를 호출하여 비동기 연결 요청

        - RegisterConnect(SocketAsyncEventArgs args):
            - 비동기 연결 요청 수행
            - 요청이 즉시 완료되면 OnConnectCompleted 실행

        - OnConnectCompleted(object sender, SocketAsyncEventArgs args):
            - 연결이 성공하면:
                - 세션을 생성하고 Start 호출
                - OnConnected 실행
            - 실패 시 오류 출력
}</code></pre><h3 id="recvbuffer">RecvBuffer</h3>
<p>서버와 클라이언트에서 주고받는 패킷들을 처리하기 전에 임시로 저장해두는 버퍼 클래스.
바이트 배열 형식의 버퍼와 이를 읽고 쓰는데 도움을 주는 유틸리티 함수들로 되어 있다.</p>
<p><strong>스도코드</strong></p>
<pre><code>클래스 RecvBuffer
{
    멤버 변수:
        - _buffer: 데이터 저장용 버퍼 (ArraySegment&lt;byte&gt;)
        - _readPos: 읽기 위치 커서
        - _writePos: 쓰기 위치 커서

    속성:
        - DataSize: 현재 버퍼에 저장된 데이터 크기
        - FreeSize: 버퍼에 남은 공간 크기
        - ReadSegment: 현재 읽을 수 있는 데이터 범위 (ArraySegment&lt;byte&gt;)
        - WriteSegment: 데이터를 받을 수 있는 범위 (ArraySegment&lt;byte&gt;)

    메서드:
        - RecvBuffer(int bufferSize):
            - 버퍼를 bufferSize 크기로 초기화

        - Clean():
            - 데이터가 없으면 커서를 초기화
            - 데이터가 남아 있으면 앞으로 이동하여 정리

        - OnRead(int numOfBytes):
            - numOfBytes만큼 읽기 커서를 이동
            - 유효성 검사 후 성공 여부 반환

        - OnWrite(int numOfBytes):
            - numOfBytes만큼 쓰기 커서를 이동
            - 유효성 검사 후 성공 여부 반환
}</code></pre><h3 id="session">Session</h3>
<p>세션 클래스는 네트워크에 연결되어 있는 각 객체들의 상태를 저장 및 변경하는 클래스이다.</p>
<p><strong>스도코드</strong></p>
<pre><code>클래스 Session (추상 클래스)
{
    멤버 변수:
        - _socket: 연결된 클라이언트 소켓
        - _disconnected: 연결 상태를 나타내는 플래그
        - _lock: 멀티스레드 동기화를 위한 객체

        - _sendQueue: 전송할 데이터를 저장하는 큐
        - _pendingList: 현재 전송 중인 데이터 리스트
        - _sendArgs: 비동기 전송을 위한 SocketAsyncEventArgs

        - _recvArgs: 비동기 수신을 위한 SocketAsyncEventArgs
        - _recvBuffer: 수신 데이터를 저장할 RecvBuffer 객체

    #region 가상 함수들
        - OnConnected(EndPoint endPoint): 클라이언트가 연결될 때 호출되는 함수
        - OnRecv(ArraySegment&lt;byte&gt; buffer): 데이터를 받을 때 실행되는 함수 (리턴값: 처리한 데이터 크기)
        - OnSend(int numOfBytes): 데이터를 성공적으로 전송했을 때 호출되는 함수
        - OnDisconnected(EndPoint endPoint): 클라이언트 연결이 종료될 때 호출되는 함수
    #endregion

    메서드:
        - Clear():
            - _sendQueue와 _pendingList를 초기화

        - Start(Socket socket):
            - _socket 할당 및 이벤트 핸들러 등록
            - 수신 시작 (RegisterRecv 호출)

        - Send(List&lt;ArraySegment&lt;byte&gt;&gt; sendBuffList):
            - 데이터가 존재하지 않으면 반환
            - sendBuffList의 데이터를 _sendQueue에 추가
            - _pendingList가 비어 있다면 RegisterSend 호출

        - Send(ArraySegment&lt;byte&gt; sendBuff):
            - _sendQueue에 데이터 추가
            - _pendingList가 비어 있다면 RegisterSend 호출

        - Disconnect():
            - 이미 연결이 종료되었으면 반환
            - OnDisconnected 호출 후 소켓 종료
            - Clear() 실행하여 내부 데이터 정리

    #region 네트워크 통신
        - RegisterSend():
            - 연결이 종료되었으면 반환
            - _sendQueue에서 데이터를 꺼내 _pendingList에 추가
            - _sendArgs에 데이터 설정 후 비동기 전송 시도
            - 만약 전송이 즉시 완료되었다면 OnSendCompleted 실행

        - OnSendCompleted(object sender, SocketAsyncEventArgs args):
            - 전송이 성공하면:
                - _pendingList 초기화
                - OnSend 호출
                - _sendQueue에 남은 데이터가 있으면 RegisterSend 호출
            - 전송 실패 시 Disconnect 실행

        - RegisterRecv():
            - 연결이 종료되었으면 반환
            - _recvBuffer의 버퍼를 초기화하고 비동기 수신 요청
            - 요청이 즉시 완료되면 OnRecvCompleted 실행

        - OnRecvCompleted(object sender, SocketAsyncEventArgs args):
            - 데이터 수신이 성공하면:
                - _recvBuffer의 Write 커서를 이동
                - OnRecv을 호출하여 데이터를 처리한 크기를 받아옴
                - 데이터가 잘못 처리되었으면 Disconnect 실행
                - Read 커서를 이동한 후 RegisterRecv 재호출
            - 데이터 수신이 실패하면 Disconnect 실행
    #endregion
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[게임 서버 개발 후기]]></title>
            <link>https://velog.io/@_hoya_/Game-Server-Study-Review</link>
            <guid>https://velog.io/@_hoya_/Game-Server-Study-Review</guid>
            <pubDate>Fri, 14 Feb 2025 10:53:59 GMT</pubDate>
            <description><![CDATA[<p><a href="https://www.inflearn.com/course/%EC%9C%A0%EB%8B%88%ED%8B%B0-mmorpg-%EA%B0%9C%EB%B0%9C-part9">참고 강의</a></p>
<p><a href="https://youtu.be/lcivCirbFzw">데모 영상</a></p>
<p><a href="https://velog.io/@starkshn/%EC%84%9C%EB%B2%84-OT">참고 문서</a></p>
<hr>
<h2 id="새로-학습한-부분">새로 학습한 부분</h2>
<p>로그인부터 게임플레이 처리까지 게임 서버에 관한 전반적인 내용을 학습할 수 있었다.</p>
<h3 id="네트워크-코어">네트워크 코어</h3>
<p><a href="https://velog.io/@_hoya_/Network-Core">기술 벨로그</a></p>
<p>C#에서 TCP소켓을 이용해 네트워크를 연결하는 스크립트를 코어 라이브러리화 하여 각각 서버와 클라이언트에 적용하였다.</p>
<h3 id="패킷-핸들러">패킷 핸들러</h3>
<p><a href="https://velog.io/@_hoya_/Packet-And-PacketHandle">기술 벨로그</a></p>
<p>Google Protobuf를 이용한 패킷 생성 자동화를 하였고, 생성된 패킷에 대한 패킷 핸들러 스크립트를 작성하였다.</p>
<h3 id="멀티스레딩과-스케줄러">멀티스레딩과 스케줄러</h3>
<p>lock을 이용한 기초적인 멀티스레딩부터 JobQueue를 이용한 일감 처리 방법 등 멀티스레딩 구현 방식을 공부하였다.
또한 서버의 각 기능별로 스레드 분배와 각 기능 스레드의 연동 방식 또한 학습하였다.</p>
<h3 id="db">DB</h3>
<p>MSSQL Server를 EF Core를 이용해 C# 서버에 연동하여 사용하는 방법을 학습하였다.</p>
<h3 id="게임-로직">게임 로직</h3>
<p>기존의 클라이언트에서 처리하던 게임 로직을 서버에서 처리하게끔 구현하였다.</p>
<hr>
<h2 id="후기">후기</h2>
<p>단순히 유니티 엔진만 다루는 클라이언트 프로그래머로 남고 싶지 않아 퇴사하고 무작정 서버 강의를 구매해 공부하기 시작했다.
서버 코어 - DB - 웹 서버 기초 - 게임 서버 콘텐츠 - 대형 구조 순으로 공부하면서 단순 서버뿐 아니라 서버 외 여러 분야도 공부할 수 있어 꽤 유익한 시간이었다.
또한 유니티 클라이언트에서도 각종 매니저 시스템 등 여러 유틸리티 프로그램들을 다루며 클라이언트 프로그래밍 능력 또한 높일 수 있었다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[오브젝트 - 컴포넌트 에디팅 시스템]]></title>
            <link>https://velog.io/@_hoya_/Object-Component-Edit-System</link>
            <guid>https://velog.io/@_hoya_/Object-Component-Edit-System</guid>
            <pubDate>Thu, 11 Jul 2024 05:52:44 GMT</pubDate>
            <description><![CDATA[<p>커스텀 에디터에서 오브젝트의 에디팅에 다양한 확장성을 부여하기 위해
현재 유니티에서 사용하고 있는 컴포넌트 시스템을 유사하게 구현하였다.</p>
<hr>
<h2 id="개요">개요</h2>
<p>유니티 에디터와 거의 유사한 오브젝트 커스터마이징을 수행하기 위해서는 오브젝트와 컴포넌트를 분리시키고, 이에 대한 에디팅 방식을 각각 구현해야 하였다.
대략적인 에디팅 사이클은 다음과 같았다.
<img src="https://velog.velcdn.com/images/_hoya_/post/f3cb88d8-bff1-4ea4-89c6-9b0afcc39dba/image.png" alt=""></p>
<p>이를 충족하기 위해 다음과 같이 클래스를 작성하였다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/2458de03-6d7d-43f9-a6c4-5a608511d09e/image.png" alt=""></p>
<h2 id="ieditable">IEditable</h2>
<p>모든 에디팅 가능한 개체에 붙어있는 인터페이스이다.</p>
<p>프로퍼티</p>
<ul>
<li>ObjectData : 에디터 내에 존재하는 모든 개체가 필수로 가지는 정보들이다.</li>
</ul>
<p>함수</p>
<ul>
<li>Select : 사용자가 해당 오브젝트를 선택했을 때 수행할 작업을 정의한다. 관련 UI 팝업 등 에디팅 사전 준비작업을 수행한다.</li>
<li>StartEdit : 사용자가 해당 오브젝트의 에디팅을 시작할 때 수행할 작업을 정의한다. 커맨드 생성 등의 작업을 수행한다.</li>
<li>OnEdit : 사용자가 마우스 등을 이용해 오브젝트를 실시간으로 에디팅할 때 어떤 값을 수정할 지에 대해 정의한다.</li>
<li>FinishEdit : 사용자가 해당 오브젝트의 에디팅을 종료했을 때 작업을 정의한다. 생성한 커맨드에 대한 처리 작업 등을 수행한다.</li>
<li>UnSelect : 사용자가 해당 오브젝트를 선택 해제했을 때 수행할 작업을 정의한다. UI 종료 등의 작업을 수행한다.</li>
<li>Save : 해당 오브젝트 및 컴포넌트 데이터를 저장할 때 사용하는 함수이다.</li>
<li>Delete : 해당 오브젝트를 씬에서 삭제할 때 저장되어 있던 데이터도 삭제시킬 때 사용하는 함수이다.</li>
</ul>
<h2 id="오브젝트">오브젝트</h2>
<p>오브젝트는 그 오브젝트가 소유하고 있는 전체 컴포넌트들의 리스트를 갖고 있다.
또한 해당 오브젝트들이 실제로 작동할 수 있도록 모노비헤이비어 함수들 또한 갖고 있다.</p>
<h2 id="컴포넌트">컴포넌트</h2>
<p>컴포넌트 스크립트의 모노비헤이비어 함수는 오브젝트에서 실행된다.</p>
<h2 id="컴포넌트-ui">컴포넌트 UI</h2>
<p>컴포넌트 UI는 UI 매니저에서 각각의 컴포넌트에 대한 UI Prefab을 갖고 있다가
사용자가 오브젝트를 <code>Select</code>할 시 오브젝트가 갖고 있는 컴포넌트들에 대한 프리팹을 생성하고, 해당 컴포넌트와 연동시킨다.</p>
<h2 id="데이터-저장-방식">데이터 저장 방식</h2>
<p>오브젝트 매니저에서 각각의 오브젝트와 컴포넌트에 대한 데이터들을 오브젝트 ID를 키로 하는 딕셔너리에 저장해두고, 이를 저장 및 불러오기하여 사용한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[커스텀 에디터 개발 후기]]></title>
            <link>https://velog.io/@_hoya_/Custom-Editor-Review</link>
            <guid>https://velog.io/@_hoya_/Custom-Editor-Review</guid>
            <pubDate>Wed, 10 Jul 2024 11:13:41 GMT</pubDate>
            <description><![CDATA[<p>유니티 엔진을 이용해 다음 기능을 수행할 수 있는 커스텀 에디터를 구현하였다.</p>
<ul>
<li>터레인 에디팅</li>
<li>데이터 저장 및 불러오기</li>
<li>커맨드 히스토리(Undo / Redo)</li>
<li>오브젝트 소환 및 삭제</li>
<li>오브젝트 컴포넌트 에디팅</li>
<li>오브젝트 데이터 서버 저장 및 불러오기</li>
</ul>
<hr>
<h2 id="새로-학습한-부분">새로 학습한 부분</h2>
<h3 id="커맨드-히스토리-구현">커맨드 히스토리 구현</h3>
<p><a href="https://velog.io/@_hoya_/Unity-Implement-CommandHistory">기술 벨로그</a></p>
<p>커맨드 패턴을 이용해 에디팅 히스토리 시스템을 구현했다.
각 에디팅 대상에 대한 정보를 콘크리트 커맨드에 입력하고,
이를 커맨드 매니저에 저장 및 불러오기 하는 시스템이다.</p>
<hr>
<h2 id="직접-개발한-부분">직접 개발한 부분</h2>
<h3 id="터레인-에디팅">터레인 에디팅</h3>
<p>유니티에서 스크립트를 이용해 터레인을 에디팅하는 기능을 구현하였다.
유니티는 터레인 데이터를 높이, 텍스처 등등으로 나누어 각각 배열로 저장해두는데
이에 접근하여 터레인을 스크립트를 이용해 플레이모드에서도 에디팅할 수 있다.</p>
<h3 id="오브젝트-컴포넌트-에디팅">오브젝트-컴포넌트 에디팅</h3>
<p><a href="https://velog.io/@_hoya_/Object-Component-Edit-System">기술 벨로그</a></p>
<p>오브젝트 생성/삭제/트랜스폼 에디팅을 구현하였다.
오브젝트 에디팅의 확장성을 위해 컴포넌트 시스템 또한 구현하였다.
트랜스폼은 컴포넌트 시스템을 바탕으로 구현하였다.</p>
<hr>
<h2 id="좋았던-부분">좋았던 부분</h2>
<h3 id="데이터-처리-실력-향상">데이터 처리 실력 향상</h3>
<p>프로젝트 특성상 파일 시스과 직렬화, 데이터 구조 정립 및 사용 방식을 많이 고민했고, 여러 방법을 시도해 보았다.
또한 인터페이스와 형변환을 많이 사용하여 이와 관련해 코드 확장성을 넓히는 방식 또한 많이 고안하였다.
이를 이용해 터레인, 오브젝트, 컴포넌트의 데이터 생성/삭제/저장/불러오기 작업을 구현하였다.</p>
<hr>
<h2 id="느낀-점">느낀 점</h2>
<p>지금까지 프로젝트들을 수행하면서 얻은 지식들을 정리할 수 있는 프로젝트였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로블록스 루아 스크립트 고급]]></title>
            <link>https://velog.io/@_hoya_/Roblox-Lua-Expert</link>
            <guid>https://velog.io/@_hoya_/Roblox-Lua-Expert</guid>
            <pubDate>Tue, 02 Apr 2024 08:43:31 GMT</pubDate>
            <description><![CDATA[<p><a href="https://youtube.com/playlist?list=PL6Zm_z30gEKTAtArbq0eScxHS08MrbspP&amp;si=f_fXjTa8F0bSdMub">강의 재생목록</a></p>
<p>C계열 언어와 유니티를 익히고, <a href="https://velog.io/@_hoya_/Roblox-Lua-Basic">기초 강좌</a>와 <a href="https://velog.io/@_hoya_/Roblox-Lua-Advanced">중급 강좌</a>를 공부한 사람 기준으로 작성했습니다.</p>
<hr>
<h2 id="01renderstepped-update">01.RenderStepped, update</h2>
<p>한 프레임이 넘어갈 때마가 호출되는 이벤트로 로컬 스크립트에서만 사용할 수 있다.
이전의 무한 반복문으로 작성했던 지속적인 함수들을 RenderStepped 이벤트를 이용해 변경해주면 부드러운 움직임 등을 얻을 수 있다.</p>
<pre><code>local RunService = game:GetService(&quot;RunService&quot;)

function update(step)
 script.Parent.Rotation += 60 * step
end

RunService.RenderStepped:Connect(update)</code></pre><p>RenderStepped 이벤트에는 deltaTime매개변수가 입력된다. 매개변수명은 주로 <code>step</code>을 사용한다.
deltaTime은 한 프레임 사이에 몇 초가 지났느냐를 뜻한다.</p>
<hr>
<h2 id="02renderstepped-stepped-heartbeat">02.RenderStepped, Stepped, Heartbeat</h2>
<p><code>Stepped</code>, <code>Heartbeat</code>는 서버 스크립트에서도 사용할 수 있는 함수이다.</p>
<p>플레이어 키보드 입력 감지 - 서버 맵 클라이언트로 복사 - 캐릭터 애니메이션 업데이트 - 물리엔진 게산 등등 한 프레임 안에서도 작업들이 순서대로 실행된다.</p>
<p>로컬 스크립트에서 <code>RenderStepped</code>는 캐릭터 조작, 물리, 카메라 등이 계산되기 전에 호출되는 함수이다. 따라서 캐릭터 조작, 물리, 카메라 등에 간섭하기 위해서는 <code>RenderStepped</code>함수를 사용해야 한다.</p>
<p><code>Stepped</code>는  물리엔진이 계산되기 전에 호출되는 함수이다. 따라서 서버에 존재하는 파트나 모델에서 물리에 간섭하기 위해서는 <code>Stepped</code>함수를 사용해야 한다.
대강 <code>RenderStepped</code> -&gt; 플레이어 캐릭터/카메라 -&gt; <code>Stepped</code> -&gt; 물리엔진 순서로 호출된다.</p>
<p><code>RenderStepped</code>는 캐릭터/파트/모델의 위치에 영향을 주는 함수를 이벤트에 바인딩해 사용하고, <code>Stepped</code>, <code>Heartbeat</code> 영향을 받는 함수를 이벤트에 바인딩해 사용한다.</p>
<pre><code>local part = workspace.Part
local RunService = game:GetService(&quot;RunService&quot;)
function update(step)
 part.CFrame = part.CFrame * CFrame.Angles(0,math.rad(180) * step ,0)
end
RunService.Stepped:Connect(update)</code></pre><p><code>Heartbeat</code>는 주로 GUI에 사용한다.</p>
<hr>
<h2 id="bindtorenderstep-renderstepped-주의사항">BindToRenderStep, RenderStepped 주의사항</h2>
<h3 id="bindtorenderstep">BindToRenderStep</h3>
<p><code>BindToRenderStep</code>함수는 <code>RenderStepped</code>와 같이 로컬 스크립트에서 사용할 수 있는 이벤트 함수이지만, <code>RenderStepped</code>와 다르게 호출 순서를 개발자가 지정할 수 있다.
또한 <code>UnbindFromRenderStep()</code>함수로 바인드한 이벤트를 해제할 수 있다.</p>
<pre><code>local RunService = game:GetService(&quot;RunService&quot;)

local a= 0
function update(step)
   a += 1
   script.Parent.Rotation += 60 * step
end

RunService:BindToRenderStep(&quot;Bind&quot;, 298, update)

RunService:UnbindFromRenderStep(&quot;Bind&quot;)</code></pre><p>로블록스 내 시스템 함수 호출 순서는 <code>Enum.RenderPriority</code>에 선언되어 있다.
자세한 사항은 <a href="https://create.roblox.com/docs/ko-kr/studio/microprofiler/task-scheduler">디벨로퍼 허브</a>에서 확인할 수 있다.</p>
<h3 id="renderstepped-함수들의-주의사항">RenderStepped 함수들의 주의사항</h3>
<ul>
<li><code>BindToRenderStep</code>함수들에는 이미 스케줄된 호출 순서를 사용하면 안 된다.</li>
<li><code>UnbindFromRenderStep</code>에 바인드하지 않은 함수 혹은 이미 언바인드 된 함수를 입력하면 오류가 발생한다.</li>
<li><code>RenderStepped</code>계열 함수에 이벤트를 바인드할 때 이벤트 안에 <code>Wait()</code>와 같이 스크립트를 대기시키는 기능을 추가하면 안 된다. 로블록스의 API 문서에서 <code>Yielding Function</code>이라 지정된 함수들은 모두 스크립트를 대기시키는 함수들이다.</li>
</ul>
<hr>
<h2 id="04모듈-스크립트-기초">04.모듈 스크립트 기초</h2>
<p>모듈 스크립트는 여러 곳에서 사용하는 함수나 변수들을 저장해주는 스크립트이다.
&#39;개체 삽입&#39; 기능을 이용해 <code>ModuleScript</code>를 추가하여 생성할 수 있다.</p>
<p>모듈 스크립트의 기본 형식은 다음과 같다. 모듈 이름과 모듈 스크립트 파일 이름을 동일하게 해주는 것이 정리에 이롭다.</p>
<pre><code>local module = {}

return module</code></pre><p>킬 파트를 모듈 스크립트화 한 예시는 다음과 같다.</p>
<pre><code>local KillPartHandler = {}

-- 킬 파트를 활성화/비활성화 해주는 변수
KillPartHandler.Enabled = true

-- 특정 파트가 들어오면 그 파트의 캐릭터를 찾아 kill하는 함수
function KillPartHandler.KillCharacterFromPart(hit)
   local Humanoid = hit.Parent:FindFirstChild(&quot;Humanoid&quot;)
   if Humanoid and KillPartHandler.Enabled then
      Humanoid.Health = 0
   end
end

return KillPartHandler</code></pre><p>모듈 스크립트를 불러오는 함수는 <code>require()</code>이다.
킬 파트를 사용하는 스크립트와 제한하는 스크립트 예시는 다음과 같다.</p>
<pre><code>--킬파트 스크립트
local KillPartHandler = require(workspace.KiilPartHandler)

script.Parent.Touched:Connect(function(hit)
 KillPartHandler.KillCharacterFromPart(hit)
end)

--킬파트 끄는 스크립트
local KillPartHandler = require(workspace.KiilPartHandler)

script.Parent.Touched:Connect(function(hit)
 KillPartHandler.Enabled = false
end)</code></pre><hr>
<h2 id="05모듈-스크립트-기초2">05.모듈 스크립트 기초2</h2>
<p>모듈 스크립트는 스스로 실행되지 않고, 서버 혹은 로컬 스크립트에서 <code>require</code>로 불러 실행시켜줘야 작동한다.
모듈 스크립트는 서버와 클라이언트 각각에서 따로 돌아간다. 따라서 같은 모듈 스크립트를 불러와도 서버 스크립트와 로컬 스크립트 내에서 모듈 스크립트의 변수는 공유되지 않는다.
따라서 모듈 스크립트는 서버에서, 혹은 로컬에서 호출되었을 때를 분리해서 구현하게 된다.</p>
<pre><code>local module = {}

local RunSevice = game.GetService(&quot;RunService&quot;)

RunSevice:IsStudio() -- 로블록스 스튜디오에서 작동중인지

function module.func()
    if RunSevice:IsServer() then
        -- 서버 작동 스크립트
    else
        -- 클라이언트 작동 스크립트
    end
end

return module</code></pre><p>모듈 스크립트에서도 이벤트를 설정할 수 있다. 단 다른 스크립트에서 모듈 스크립트를 불러올 때에만 사용 가능하다.</p>
<pre><code>-- 모듈 스크립트
local module = {}

workspace.BasePlate.Touched:Connect(function()
    print(&quot;BasePlate&quot;)
end)

return module</code></pre><pre><code>-- 모듈 스크립트 호출
require(workspace.ModuleScript)</code></pre><p>모듈에서 실질적으로 사용하는 함수가 하나밖에 없을 때에는 모듈 선언을 생략할 수 있다.</p>
<pre><code>-- 모듈 스크립트
function f()
    print(&quot;function&quot;)
end

return f</code></pre><pre><code>-- 모듈 스크립트 사용
local name = require(workspace.ModuleScript)
name.f()</code></pre><p>모듈 스크립트를 &#39;우클릭 - 로블록스에 저장&#39;을 이용해 도구 상자에 업로드하고, 도구 상자에서 생성된 모듈ID를 이용해 모듈 스크립트를 불러올 수 있다.
이를 이용해 바이러스 공격에 대한 보안과 모듈 배포의 용이함을 챙길 수 있다.</p>
<p>모듈 스크립트는 주로 &#39;ReplicatedStorage&#39;와 &#39;ServerScriptService&#39;폴더에 저장해 두는데, 로컬과 서버 스크립트에서 모두 호출할 때에는 &#39;ReplicatedStorage&#39;에, 서버 스크립트에서만 호출할 때에는 &#39;ServerScriptService&#39;폴더에 저장한다.</p>
<hr>
<h2 id="06magnitude-unit">06.Magnitude, Unit</h2>
<h3 id="magnitude">Magnitude</h3>
<p>두 포지션 사이의 거리를 구할 때 <code>Magnitude</code>를 사용해 구할 수 있다.</p>
<pre><code>local pos = workspace.red.Position - workspace.blue.Position
print(pos.Magnitude)</code></pre><h3 id="unit">Unit</h3>
<p>파트의 크기를 1로 정규화하려할 때 <code>Unit</code>을 사용해 구할 수 있다.</p>
<pre><code>
workspace.Part11.Size = workspace.Part1.Size.Unit
workspace.Part22.Size = workspace.Part2.Size.Unit
workspace.Part33.Size = workspace.Part3.Size.Unit

print(workspace.Part11.Size.Magnitude) -- 1 출력
print(workspace.Part22.Size.Magnitude) -- 1 출력
print(workspace.Part33.Size.Magnitude) -- 1 출력</code></pre><hr>
<h2 id="07-pcall">07. pcall</h2>
<p><code>pcall()</code>함수는 pcall함수로 감싼 안쪽에서 에러가 발생해도 바깥쪽에 영향을 주지 않도록 하는 함수이다.
단 스크립트 문법 오류시에는 pcall함수에서도 에러가 발생한다.</p>
<pre><code>print(111111111)

--이렇게 쓰면 에러나겠지?
script.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent.Parent
 = script

print(222222222)</code></pre><p>또한 pcall함수는 다음과 같은 변수들을 반환하여 어떠한 에러가 발생하였는지 알려주는 기능 또한 존재한다.</p>
<ul>
<li>성공 여부(bool)</li>
<li>에러 메세지(string)<pre><code>success, errorMessage = pcall(function()
  script.Parent.Parent.Parent.Parent.Parent.Parent.Parent = script
end)
if not success then
  print(errorMessage)
end</code></pre></li>
</ul>
<p>pcall 함수 외부에서 함수를 선언한 후에 pcall의 매개변수로 함수의 이름을 작성해 사용할 수 있다. 이 때 함수의 매개변수는 pcall의 다음 매개변수로 추가해주면 된다.</p>
<pre><code>-- print(&quot;aaaaaa&quot;, &quot;bbbbbb&quot;)를 pcall에 추가한 상황
success, errorMessage = pcall(print, &quot;aaaaaa&quot;, &quot;bbbbbb&quot;)
if not success then
   print(errorMessage)
end</code></pre><p><code>xpcall(함수1, 에러 담당 함수, ...)</code>함수는 함수1에서 오류가 발생하면 에러 담당 함수가 대신 호출되는 형식의 pcall 함수이다.</p>
<pre><code>function HandleError(errorMessage)
   print(errorMessage)
end
-- print함수가 실패하면 HandleError 함수가 실행된다
xpcall(print, HandleError, &quot;aaaaaa&quot;, &quot;bbbbbb&quot;)</code></pre><p>pcall함수는 주로 반복적인 데이터 저장에서 오류를 방지하는 데에 사용한다.
하지만 최적화가 되어있지 않기 때문에 상황에 따라 사용에 유의해야 한다.</p>
<pre><code>local DataStore = game:GetService(&quot;DataStoreService&quot;):GetDataStore(&quot;MyData&quot;)
local success, errorMessage
local count = 1 

repeat
   if count == 1 then
      warn(&quot;재시도 횟수:&quot;, count, &quot; / 에러:&quot;, errorMessage)
      wait(7) 
   end

   success, errorMessage = pcall(DataStore.GetAsync, DataStore, &quot;key&quot;)
   count = count + 1 
until success</code></pre><hr>
<h2 id="08-09코루틴">08-09.코루틴</h2>
<p>코루틴은 한 스크립트 안에서 동시에 여러 동작을 수행하게 만들고 싶을 때 사용한다.</p>
<p>코루틴을 생성할 때에는 <code>coroutine.create(실행시킬 함수명)</code>함수를
코루틴을 실행시킬 때에는 <code>coroutine.resume(실행시킬 코루틴, 함수의 매개변수...)</code>를 사용한다.
혹은 <code>coroutine.wrap(실행시킬 함수명)</code>으로 일반 함수를 코루틴 함수로 래핑해 사용할 수도 있다.</p>
<p>한번 코루틴을 수행한 함수는 다시 재활용할 수 없다.</p>
<p>코루틴을 수행한 함수 또한 값을 반환할 수 있다. 이 때 코루틴 내에서 wait와 같은 대기 함수를 사용하면 값 반환이 수행되지 않는다.
<code>coroutine.resume()</code>함수는 pcall함수와 비슷하게 첫 번째로 코루틴의 에러 메시지가, 그 이후로 코루틴 함수의 반환값이 반환된다.</p>
<pre><code>local function ChangeColor(part,p,d)
   for i=1, 100 do
      part.BrickColor = BrickColor.random()
   end
   return part
end

local c1 = coroutine.wrap(ChangeColor)
local success1, result1 = coroutine.resume(c1, workspace.Left)

local c2 = coroutine.create(ChangeColor)
local success2, result2 = coroutine.resume(c2, workspace.Middle)

local c3 = coroutine.wrap(ChangeColor)
local result3 = c3(workspace.Left)</code></pre><p><code>coroutine.resume()</code>으로 감싼 코루틴 함수는 사용하는 중간에 <code>coroutine.yield()</code>로 일시정지 시킬 수 있다. 일시정지시킨 코루틴은 다시 <code>coroutine.resume()</code>으로 재가동시킬 수 있다.</p>
<pre><code>local function ChangeColor(part)
    while true do
        for i=1, 100 do
            wait()
            part.BrickColor = BrickColor.random()

        end
        coroutine.yield()
    end
    return part
end

local c2 = coroutine.create(ChangeColor)
coroutine.resume(c2, workspace.Middle)
coroutine.resume(c2, workspace.Middle)</code></pre><p><code>coroutine.status()</code>함수를 이용해 현재 코루틴 함수의 상황을 확인할 수 있다. 코루틴의 상황종류는 다음과 같다.
<img src="https://velog.velcdn.com/images/_hoya_/post/8c7b8c9c-21da-4602-a9b4-20de4a1aca1b/image.png" alt=""></p>
<hr>
<h2 id="10스레드-spawn">10.스레드, spawn()</h2>
<h3 id="스레드">스레드</h3>
<p>스레드는 한 번에 실행되는 코드의 흐름 단위라 생각하면 편하다.
wait()함수를 사용하면 사용한 스레드만을 대기시키는 함수이고, error가 발생해도 스레드 안에서만 발생하므로 다른 스레드에서 실행되는 함수들은 정상 작동한다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/bedfc148-283f-485f-ad50-8c281ecf725d/image.png" alt=""></p>
<h3 id="spawn">spawn()</h3>
<p>무한 반복문을 스레드를 나누어 사용하고 싶을 때 사용하는 함수이다. 코루틴보다 성능이 좋지 않아 코루틴으로 대체되었다.</p>
<pre><code>function repeatChangeColor(a)
    print(a)
    while true do
        script.Parent.BrickColor = BrickColor.random()
        wait(0.2)
    end
end

spawn(function()
    repeatChangeColor(&quot;a&quot;)
end)

print(&quot;aaaa&quot;)</code></pre><hr>
<h2 id="11cframelerp">11.CFrame.Lerp</h2>
<p>Lerp를 사용해 두 CFrame의 보간을 적용할 수 있다.</p>
<pre><code>local RedPart = workspace.RedPart
local BluePart = workspace.BluePart

local Part = workspace.Part

-- BluePart와 RedPart의 사이 정 중앙에 Part를 위치
Part.CFrame = RedPart.CFrame:Lerp(BluePart.CFrame, 0.5)

-- Part가 BluePart를 따라다니게 만들기
while wait() do
 Part.CFrame = Part.CFrame:Lerp(BluePart.CFrame, 0.25)
end</code></pre><p>이를 이용해 구현된 마우스를 따라 움직이는 머리 스크립트는 다음과 같다.
<img src="https://velog.velcdn.com/images/_hoya_/post/39a21d9a-095c-42d2-bbcf-f1d6f11dace5/image.png" alt=""></p>
<hr>
<h2 id="12bindableremote-function">12.Bindable/Remote Function</h2>
<h3 id="bindable-function">Bindable Function</h3>
<p>Bindable Function은 어떠한 스크립트에서 생성된 함수를 다른 스크립트에서 실행 가능하게 해주는 것은 동일하지만, Event와 다르게 매개변수 제공 및 반환값 획득이 가능하다.
로블록스에서 탐색기에 <code>Bindable Function</code>을 추가하여 생성할 수 있다.</p>
<p>ServerStorage에 Bindable Function을 추가하고 A스크립트 바인더블 실행, B스크립트에서 바인더블 할당을 하는 예시는 다음과 같다.</p>
<ul>
<li>A스크립트<pre><code>local BF = game.ServerStorage.BindableFunction
</code></pre></li>
</ul>
<p>-- 플레이어가 터치할 시
script.Parent.Touched:Connect(function()
 -- 바인더블 함수 실행 후 RandomColor 변수에 리턴 값 저장
 local RandomColor = BF:Invoke()
 wait(2)
 script.Parent.BrickColor = RandomColor
end)</p>
<pre><code>
- B스크립트</code></pre><p>-- 파트의 색 변경 후 바꾼 색을 리턴값으로 반환
local function ChangeRandomColor()
 local RandomColor = BrickColor.random()
 script.Parent.BrickColor = RandomColor
 return RandomColor
end</p>
<p>-- 바인더블 함수 연결
local BF = game.ServerStorage.BindableFunction
BF.OnInvoke = ChangeRandomColor</p>
<pre><code>
바인더블 함수는 바인드시킨 일반 함수에 오류가 있을 시 리턴받는 스크립트에도 오류가 발생한다. 또한 이벤트보다 처리속도가 느리다.

또한 바인더블 함수는 `FireAllClients()`함수로 모든 서버 내 파트에게 적용된 바인더블 함수를 트리거시킬 수 있다.

### Remote Function

바인더블 펑션의 사촌지간으로 탐색기에 `Remote Function`을 추가하여 생성할 수 있다.
로컬/서버 간 연결이 가능하여 Replicated Storage에 주로 만든다.

리모트 펑션은 로컬-&gt;서버 전송과 서버-&gt;로컬 전송 시 스크립트 작성 방식이 다르다.

- 로컬에서 서버로 전송할 때
  - `InvokeServer(매개변수)`를 사용해 이벤트를 호출한다.
  - 서버에서 이벤트를 바인드할 때 첫 번째 매개변수로는 호출한 플레이어의 정보가 들어온다.
</code></pre><p>-- 로컬 스크립트에서 이벤트 호출
local RF = game.ReplicatedStorage.RemoteFunction</p>
<p>script.Parent.Activated:Connect(function()
 local Name = RF:InvokeServer(&quot;aaaaa&quot;)
 script.Parent.Text = Name
end)</p>
<hr>
<p>-- 서버 스크립트에서 이벤트 바인드
local RF = game.ReplicatedStorage.RemoteFunction
RF.OnServerInvoke = function(player, aa)
 script.Parent.BrickColor = BrickColor.random()
 return script.Parent.BrickColor.Name
end</p>
<pre><code>
- 서버에서 로컬로 전송할 때
  - `InvokeClient(대상 플레이어, 매개변수...)`를 사용해 특정 플레이어에 함수를 발동시킨다.
</code></pre><p>-- 서버 스크립트에서 이벤트 호출
local RF = game.ReplicatedStorage.RemoteFunction
local player = game.Players.PlayerAdded:Wait()
wait(3)
RF:InvokeClient(player, &quot;aaaa&quot;, &quot;bbbb&quot;)</p>
<hr>
<p>-- 로컬 스크립트에서 이벤트 바인드
local RF = game.ReplicatedStorage.RemoteFunction
RF.OnClientInvoke = function(aaa, bbb)
 script.Parent.BackgroundColor = Color3.new(0.403922, 1, 1)
end</p>
<pre><code>
바인더블/리모트 펑션 모두 다음과 같은 매개변수는 제한된다.
- 배열과 딕셔너리가 혼합된 테이블은 배열만 매개변수로 적용
- 딕셔너리는 문자열 키만을 가진 딕셔너리만 매개변수로 적용
- 메타테이블은 전달이 불가능

![](https://velog.velcdn.com/images/_hoya_/post/c3b450a1-e1bf-40eb-8d66-382e8d1e4d95/image.png)


---

## 13.문자열 함수, string 라이브러리

- `tostring()` : 숫자를 집어넣으면 문자열을 반환
- `tonumber()` : 문자열을 집어넣으면 숫자를 반환, 숫자가 아닌 문자가 끼어 있을 시 nil 반환

- 백슬래시를 이용해 문자열 안에 따옴표와 같은 특수 기호들을 넣을 수 있다.` &quot; \&quot;abc\&quot; &quot; `
  - 큰따옴표, 작은따옴표, 대괄호, 백슬래시 등등

- `string.char(아스키 코드...)` : 아스키 코드들을 집어넣고 문자열로 반환된 값을 얻을 수 있다.

- `string.len(문자열)` : 문자열의 길이 반환
- `string.lower(문자열)` : 문자열 내 대문자들을 모두 소문자로 바꿔줌
- `string.upper(문자열)` : 문자열 내 소문자들을 모두 대문자로 바꿔줌
- `string.reverse(문자열)` : 문자열을 뒤집음 (문자열 -&gt; 열자문)

- `string.split(문자열, 기준 문자)` : 기준 문자를 기준으로 문자열을 분할시킨다.</code></pre><p>str = string.split(&quot;가격:500&quot;, &quot;:&quot;)
print(str[1], str[2]) -- &quot;가격&quot;, &quot;500&quot;</p>
<pre><code>- `string.find(문자열, 찾을 문자열, 찾기 시작할 위치)` : 찾을 문자열이 문자열의 몇 번째 위치에 있는지를 반환, 못 찾을시 nil 반환
- `string.match(문자열, 찾을 문자열, 찾기 시작할 위치)` : 문자열 내의 찾을 문자열을 반환, 못 찾을시 nil 반환

- `string.sub(문자열, 시작위치, 마지막위치)` : 문자열을 시작 위치부터 마지막 위치까지만 따로 나누어 반환

- `string.gsub(문자열, 변경 전 문자열, 변경 후 문자열)` : 문자열 내의 변경 전 문자열들을 변경 후 문자열들로 바꾼 후 반환

---

## 14.문자열 포매팅, 한글 조사 처리

문자열 중간 삽입, 번역 등의 이슈로 문자열의 포매팅을 수행해야 할 때가 있다.
다음과 같이 특수기호들을 문자열 중간에 적은 후 `string.format`을 이용해 문자열을 합친다.</code></pre><p>itemName = &quot;사과&quot;
message = &quot;아이템 %s를 손에 넣었다!&quot;</p>
<p>print(string.format(message, itemName))</p>
<pre><code>
한글 조사 처리를 수행하는 스크립트는 다음과 같다.</code></pre><p>local function HasBatchim(munja)
    local lastChar = string.sub(munja, -3,-1)
    local charCode = utf8.codepoint(lastChar)
    if charCode &lt; 44032 or charCode &gt; 55203 then 
        warn(munja..&quot;의&quot;..lastChar..&quot;: 완성형 한글이 아님&quot;)
        return nil
    end
    if (charCode - 44032) % 28 == 0 then
        return false
    else
        return true
    end
end</p>
<p>local function josa(munja)
    if HasBatchim(munja) then
        return &quot;을&quot;
    else 
        return &quot;를&quot;
    end
end</p>
<p>function ShowMessage(itemName)
    local message = &quot;%s%s 손에 넣었다!&quot;
    print( string.format(message, itemName, josa(itemName)) )
end</p>
<p>ShowMessage(&quot;사화&quot;)
ShowMessage(&quot;귤귤&quot;)
ShowMessage(&quot;asdasdqㅂㅈ오면왐농ㄴㅇㅇㅇ&quot;)</p>
<pre><code>
---

## 15.숫자 포매팅, 타이머 표시

중괄호를 이용해 문자열 포메팅이 가능하다.</code></pre><p>local number = 3
local itemName = &quot;배&quot;
local s = <code>{itemName} {number}개를 먹었습니다.</code>
print(s) -- &quot;배 3개를 먹었습니다.&quot; 출력</p>
<pre><code>
![](https://velog.velcdn.com/images/_hoya_/post/dae4e6f2-6ba5-459d-8186-7c2b99d99c2f/image.png)

---

## 16.테이블 복사, 다차원 테이블

일반적으로 문자, 숫자 등은 깊은 복사로 값을 옮기지만, 테이블, 파트 등은 얕은 복사로 레퍼런스를 옮긴다.</code></pre><p>local a = 3
local b = a</p>
<p>print(a, b + 1) -- 3, 4 출력</p>
<p>local c = {1, 2}
local d = c
table.insert(d, 3)
print(c, d) -- {1, 2, 3}, {1, 2, 3} 출력</p>
<pre><code>
따라서 테이블을 깊은 복사를 하기 위해서는 따로 테이블 값들을 일일히 넣어주는 함수를 만들어야 한다.
다음은 재귀함수를 이용해 다차원 테이블을 깊은 복사해주는 함수의 예시이다.</code></pre><p>function cloneTable(t)
    local c = {}
    for i, v in pairs(t)do
        if type(v) == &quot;table&quot; then
            c[i] = cloneTable(v)
        else
            c[i] = v
        end
    end
    return c
end</p>
<p>local a = {{r = 0, l = 1}, {r = 2, l = 3}, 3}
local b = cloneTable(a)
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[로블록스 루아 스크립트 중급]]></title>
            <link>https://velog.io/@_hoya_/Roblox-Lua-Advanced</link>
            <guid>https://velog.io/@_hoya_/Roblox-Lua-Advanced</guid>
            <pubDate>Tue, 02 Apr 2024 08:41:11 GMT</pubDate>
            <description><![CDATA[<p><a href="https://youtube.com/playlist?list=PL6Zm_z30gEKRhtuWuq6ZhJCJ2BsgkVgaL&amp;si=615jQ7yl_YGGHejT">강의 재생목록</a></p>
<p>C계열 언어와 유니티를 익히고, <a href="https://velog.io/@_hoya_/Roblox-Lua-Basic">기초 강좌</a>를 공부한 사람 기준으로 작성했습니다.</p>
<hr>
<h2 id="01이벤트-touchedconnect">01.이벤트 Touched:Connect()</h2>
<p><a href="https://create.roblox.com/docs/ko-kr/scripting/events">이벤트</a>란 &#39;누가 밟았을 때&#39;, &#39;새 플레이어가 접속했을 때&#39;, &#39;누가 버튼을 눌렀을 때&#39; 등등 특정한 이벤트가 발생할 때 스크립트를 시작하게 만드는 것이다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/1e7a00e7-088f-4fe1-a003-897b88c70860/image.png" alt=""></p>
<p>다른 파트가 닿을 때까지 기다린 후, 닿으면 색을 랜덤색으로 변경하는 이벤트 스크립트</p>
<pre><code>part.Touched:Wait()
part.BrickColor = BrickColor.Random()</code></pre><p>다른 파트와 맟닿을 시 ChangeColor함수를 호출하는 이벤트 스크립트
이 때 맟닿은 파트는 hit 매개변수로 입력되며 Destroy()가 호출된다.</p>
<pre><code>local part = script.Parent

function ChangeColor(hit)
    part.BrickColor = BrickColor.Random()
    hit:Destroy()
end

part.Touched:Connect(ChangeColor)</code></pre><p><img src="https://velog.velcdn.com/images/_hoya_/post/9a7a5651-66c3-4349-87a9-f1438b320b3b/image.png" alt=""><img src="https://velog.velcdn.com/images/_hoya_/post/ed044bbd-a262-497c-b34d-c588ae87c28b/image.png" alt=""></p>
<hr>
<h2 id="02findfirstchild-캐릭터에만-반응하는-이벤트">02.FindFirstChild, 캐릭터에만 반응하는 이벤트</h2>
<h3 id="findfirstchild">FindFirstChild</h3>
<p>다음과 같이 workspace 내 파트를 찾을 시 파트가 없으면 에러가 발생한다.</p>
<pre><code>local part = workspace.Part
print(part) // part가 없을 시 오류 출력</code></pre><p>하지만 다음과 같이 파트를 찾을 시 파트가 없으면 <code>nil</code>이 출력된다.</p>
<pre><code>local part = workspace:FindFirstChild(&quot;Part&quot;)
print(part)</code></pre><p>이렇게 <code>FindFirstChild</code>함수를 이용해 파트를 찾게 되면 에러가 발생하지 않으므로 개체를 탐색하거나 파트의 없음을 확인할 때 사용하기 용이하다.</p>
<h3 id="캐릭터에-반응하는-이벤트">캐릭터에 반응하는 이벤트</h3>
<p>위의 <code>FindFirstChild</code>를 이용해 맞닿은 파트가 플레이어 캐릭터인지 확인 후 선택적으로 반응하는 스크립트를 작성할 수 있다.</p>
<pre><code>local part = script.Parent

function ChangeColor(hit)
    local humanoid = hit.Parent:FindFirstChild(&quot;Humanoid&quot;)
    if humanoid then
        part.BrickColor = BrickColor.Random()
    end
end

part.Touched:Connect(ChangeColor)</code></pre><hr>
<h2 id="03findfirstchild의-형제들">03.FindFirstChild의 형제들</h2>
<p>다음과 같이 <code>FindFirstChild</code>의 플래그를 true로 변경하면 바로 아래 단계의 자식뿐 아니라 모든 단계의 자식들을 탐색한다.</p>
<pre><code>print(workspace:FindFirstChild(&quot;Part&quot;, true))</code></pre><p>자식이 아닌 부모를 찾아주는 함수는 <code>FindFirstAncestor</code>이다.
이 함수 또한 이름이 동일한 부모를 찾지 못하면 <code>nil</code>을 반환한다.</p>
<pre><code>print(script:FindFirstAncestor(&quot;Part&quot;))</code></pre><p>자식 개체를 이름이 아닌 클래스네임으로 찾는 함수는 <code>FindFirstChildOfClass</code>이다.
파트 이름이 변경되어도 찾을 수 있다는 이점이 있다.</p>
<pre><code>print(workspace:FindFirstChildOfClass(&quot;Part&quot;))</code></pre><p>특정 클래스를 상속한 클래스들을 찾기 위해 사용하는 함수는 <code>FindFirstChildWhichIsA</code>이다.
자식 파트들 중 <code>BasePart</code>클래스를 상속받는 <code>Part</code>, <code>WedgePart</code>, <code>TrussPart</code>들 중 위에 하나만을 찾기 위해 다음과 같이 사용할 수 있다.</p>
<pre><code>print(workspace:FindFirstChildWhichIsA(&quot;BasePart&quot;))</code></pre><p><code>FindFirstAncestor</code>함수 또한 <code>OfClass</code>와 <code>WhichIsA</code>함수가 존재한다.</p>
<hr>
<h2 id="04변수-연산-킬파트와-데미지-파트">04.변수 연산, 킬파트와 데미지 파트</h2>
<h3 id="대입연산자">대입연산자</h3>
<p>Lua에서는 C계열과 마찬가지로 대입연산자를 사용할 수 있다.
대입 연산자는 어떠한 변수에 연산을 한 값을 다시 그 변수값으로 설정한다는 뜻이다.</p>
<pre><code>local a = 10

a += 3   // a = a + 3
a -= 3   // a = a - 3
a *= 3   // a = a * 3
a /= 3   // a = a / 3</code></pre><p>대입 연산자는 선언한 변수뿐만 아니라 파트의 인자값 등에도 사용할 수 있다.</p>
<h3 id="킬파트와-데미지-파트">킬파트와 데미지 파트</h3>
<p>로블록스 플레이어의 Humanoid 파트에는 Health 속성이 있다. 이 값을 0으로 만들면 플레이어는 사망하게 된다.
<img src="https://velog.velcdn.com/images/_hoya_/post/751ecc42-85ed-40c0-bf98-916190912661/image.png" alt=""></p>
<p>닿은 플레이어를 죽게 만드는 킬 시스템은 다음과 같이 구현할 수 있다.</p>
<pre><code>local part = script.Parent

function kill(hit)
    local humanoid = hit.Parent:FindFirstChild(&quot;Humanoid&quot;)
    if humanoid then
        humanoid.Health = 0
    end
end

part.Touched:Connect(kill)</code></pre><p>데미지 파트는 <code>humanoid.Health = 0</code> 대신에 <code>humanoid:TakeDamage(데미지값)</code>을 사용하면 된다.</p>
<hr>
<h2 id="05이름-없는-함수-이벤트-쿨타임">05.이름 없는 함수, 이벤트 쿨타임</h2>
<h3 id="이름-없는-함수">이름 없는 함수</h3>
<p>재사용성 없는 함수를 선언할 필요가 있을 때(하나의 이벤트에만 연결되는 함수 등) 이름 없는 함수를 선언과 동시에 사용할 수 있다.</p>
<pre><code>local part = script.Parent

// Connect 안에 이름 없는 함수 선언
part.Touched:Connect(function(hit)
    local humanoid = hit.Parent:FindFirstChild(&quot;Humanoid&quot;)
    if humanoid then
        humanoid.Health -= 5
    end
end)</code></pre><h3 id="이벤트-쿨타임">이벤트 쿨타임</h3>
<p>이벤트 발생 조건이 상시로 부합되지만, 이벤트의 발생에 쿨타임을 두고 싶을 경우 다음과 같이 조건을 둘 수 있다.</p>
<pre><code>local part = script.Parent

local Enabled = true

// 1초마다 한 번씩 발생하도록 조건을 걸어둠
part.Touched:Connect(function(hit)
    local humanoid = hit.Parent:FindFirstChild(&quot;Humanoid&quot;)
    if humanoid and Enabled then
        Enabled = false
        humanoid.Health -= 5
        wait(1)
        Enabled = true
    end
end)</code></pre><hr>
<h2 id="06모델-옮기기-moveto">06.모델 옮기기, MoveTo()</h2>
<p>로블록스의 모델(유니티의 프리팹과 유사)은 일반적인 트랜스폼 개념이 존재하지 않는다. 따라서 모델은 이전에서 파트를 이동시키던 것처럼 트랜스폼 값을 변경해 이동시킬 수 없다.</p>
<p>대신 모델은 다음 함수를 이용해 특정 위치로 이동시킬 수 있다.</p>
<pre><code>workspace.Model:MoveTo(Vector3.new(목표 좌표값))</code></pre><p>해당 함수를 이용해 모델을 옮길 때 이미 다른 모델 혹은 파트가 자리에 위치해 있으면 이미 있던 모델/파트의 위로 이동하게 된다.
따라서 <code>MoveTo</code>를 이용해 모델을 옮길 때에는 목표 좌표값에 다른 모델이 있으면 안 된다.</p>
<p><code>MoveTo</code>함수는 workSpace 안에 속해 있는 모델들에 한해서 작동된다. 따라서 ServerStorage에서 복사해 소환하는 오브젝트들은 워크스페이스에 소속시킨 후 <code>MoveTo</code>를 이용해 이동시켜야 한다.</p>
<pre><code>local car = game.ServerStorage.Model
local clone = car:Clone()
clone.Parent = workspace
clone:MoveTo(Vector3.new(0,100,0))</code></pre><p>모델을 <code>MoveTo</code>로 이동시킬 때 위치의 기준은 모델을 선택했을 때 나오는 파란 상자(Bounding Box)의 정중앙이다.
다만 모델의 <code>Data-PrimaryPart</code>를 지정했을 때는 <code>PrimaryPart</code>의 정중앙을 기준으로 이동하게 된다.</p>
<hr>
<h2 id="07다양한-이벤트">07.다양한 이벤트</h2>
<h3 id="childadded">ChildAdded</h3>
<p>파트에 새 자식 개체가 생성 혹은 추가되었을 때 발생하는 이벤트이다.
매개변수에는 추가된 개체가 들어오게 된다.</p>
<pre><code>local part = script.Parent

part.ChildAdded:Connect(function(child)

end)</code></pre><h3 id="changed">Changed</h3>
<p>파트의 속성 중 하나가 바뀌었을 때 발생하는 이벤트이다.
매개변수에는 바뀐 속성의 이름이 문자열로 들어온다.</p>
<pre><code>part.Changed:Connect(function(property)
   if property == &quot;Position&quot; then

   end
end)</code></pre><h3 id="getpropertychangedsignal">GetPropertyChangedSignal</h3>
<p>파트의 특정 속성이 바뀌었음을 감지하기 위해 사용하는 함수이다.
매개변수는 아무것도 들어오지 않는다.</p>
<pre><code>part:GetPropertyChangedSignal(&quot;Position&quot;):Connect(function()
// position속성이 바뀌었을 때만 이벤트가 호출됨
end)</code></pre><h3 id="playeradded">PlayerAdded</h3>
<p>게임 내 &#39;플레이어&#39;파트에서만 사용할 수 있는 함수로 게임에 새 플레이어가 들어왔을 때 호출되는 함수이다.
매개변수로는 새로 들어온 플레이어 개체이다,</p>
<pre><code>// 플레이어 이름이 nofair2002이면 해당 플레이어를 강제퇴장시킨다.
game.Players.PlayerAdded:Connect(function(plr)
   if plr.Name == &quot;nofair2002&quot; then
      plr:Kick()
   end
end)</code></pre><p>플레이어 캐릭터가 스폰될 때 호출되는 <code>CharacterAdded</code>함수를 추가할 수도 있다.</p>
<pre><code>game.Players.PlayerAdded:Connect(function(plr)
   plr.CharacterAdded:Connect(function(chr)

   end)
end)</code></pre><hr>
<h2 id="08서버와-클라이언트">08.서버와 클라이언트</h2>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/14249e46-daa9-4768-b54e-d8cec2c97e42/image.png" alt=""><img src="https://velog.velcdn.com/images/_hoya_/post/dd09c060-64f4-40e5-99f4-a4dd57688321/image.png" alt=""><img src="https://velog.velcdn.com/images/_hoya_/post/d85fb304-9b44-4b31-ba64-8cd554d3783a/image.png" alt=""></p>
<hr>
<h2 id="09로컬-스크립트-localplayer">09.로컬 스크립트, LocalPlayer</h2>
<h3 id="로컬-스크립트">로컬 스크립트</h3>
<p>지금까지 작성한 Script는 서버에서 작동되는 서버 스크립트였다. Script는 탐색기 내  Workspace와 ServerScriptService에서만 실행된다.</p>
<p>이와 반대로 로컬(각 클라이언트)에서만 작동되는 LocalScript는 ReplicatedFirst, StarterGui, StarterPack, StarterPlayer의 StarterCharacterScripts와 StarterPlayerScripts에서만 작동한다.
각각 위의 폴더에 넣은 스크립트들은 게임 실행시 플레이어 스폰과 동시에 다음과 같이 폴더에 추가된다.
<img src="https://velog.velcdn.com/images/_hoya_/post/faeeca89-8b1e-429c-8618-11f94fe4bf18/image.png" alt=""></p>
<h3 id="localplayer">LocalPlayer</h3>
<p>로컬 스크립트에서는 로컬 플레이어에 접근할 수 있다. 로컬 플레이어는 클라이언트 내 본인의 플레이어를 자칭한다.</p>
<pre><code>local localplayer = game.Players.LocalPlayer</code></pre><p>서버 스크립트에서는 로컬 플레이어에 접근할 수 없지만 다음과 같이 이벤트를 발생시키는 플레이어를 특정할 수 있다.</p>
<pre><code>game.Players.PlayerAdded:Connect(function(plr)
    // 플레이어가 맵에 추가될 때 호출될 기능 추가
end)

part = workspace.Baseplate
part.Touched:Connect(function(hit)
   // 특정 파트에 닿은 플레이어를 확인하는 기능
   local plr = game.Players:GetPlayerFromCharacter(hit.Parent)
end)</code></pre><hr>
<h2 id="10waitforchild">10.WaitForChild</h2>
<h3 id="문자열-합치기">문자열 합치기</h3>
<p>Lua에서 문자열은 다음과 같이 문자열 사이에 <code>..</code>를 추가해 합칠 수 있다.</p>
<pre><code>print(&quot;문자열1&quot;..&quot;문자열2&quot;)</code></pre><p>숫자만 적혀있는 문자열들을 +하면 Lua는 문자열을 숫자로 인식해 실제 더한 값을 출력한다.</p>
<pre><code>print(&quot;2&quot;+&quot;7&quot;)   // 9 출력
print(&quot;2&quot;..&quot;7&quot;)  // 27 출력</code></pre><h3 id="waitforchild">WaitForChild</h3>
<p>서버 스크립트는 맵 로딩이 완전히 끝날 때까지 작동하지 않는다.
맵 로딩이 완전히 끝나기 전에 스크립트가 작동하면 맵 내의 오브젝트를 찾지 못하는 오류가 발생하기 때문이다.</p>
<p>하지만 로컬 스크립트에서 workspace에 접근할 때에는 문제가 발생하는데, 로컬 스크립트는 맵 로딩이 끝나기 로컬에서 자체적으로 작동되기 때문이다.
따라서 로컬 스크립트에서 workspace로 접근할 때에는 <code>WaitForChild</code>함수를 사용해 원하는 파트가 소환될때까지 대기시킨다.</p>
<pre><code>local part = workspace:WaitForChild(&quot;Baseplate&quot;)
part.BrickColor = BrickColor.new(&quot;Really black&quot;)</code></pre><p><code>WaitForChild</code>는 위와 같이 로컬에서 맵 로딩을 기다리는 용도 뿐 아니라, 게임이 돌아가는 도중에도 특정 파트가 소환될 때까지 기다리는 등에 사용할 수 있다.</p>
<p>다음과 같이 <code>WaitForChild</code>의 두 번째 매개변수로 시간(초)를 추가해주면 해당 함수는 지정해준 시간동안만 오브젝트 서칭을 하고, 그 안에 오브젝트가 소환되지 않으면 <code>nil</code>을 반환한다.</p>
<pre><code>// 5초 동안만 Part를 찾고 못 찾으면 nil을 반환
local part = workspace:WaitForChild(&quot;Part&quot;, 5)
part.BrickColor = BrickColor.new(&quot;Really black&quot;)</code></pre><p>위의 <code>WaitForChild</code>를 이용해 서버에서 소환하는 플레이트의 표면에 로컬에서만 보이는 글자를 추가하는 로컬 스크립트는 다음과 같다.</p>
<pre><code>//워크스페이스의 서페이스 탐색
local part = workspace:WaitForChild(&quot;Part&quot;)
local surfaceGui = part:WaitForChild(&quot;SurfaceGui&quot;)
local TextLabel = surfaceGui:WaitForChild(&quot;TextLabel&quot;)
// 로컬 플레이어의 이름 가져오기
local player = game.Players.LocalPlayer
local playerName = player.Name
// 서페이스에 글자 작성
TextLabel.Text = playerName..&quot;님 안녕하세요!&quot;</code></pre><p>캐릭터 또한 한번에 모든 파트가 생성되는 것이 아니라 하나하나씩 소환되는 것이기 때문에 캐릭터에 접근할 때도 <code>WaitForChild</code>를 사용해야 한다.</p>
<pre><code>game.Players.PlayerAdded:Connect(function(plr)
   plr.CharacterAdded:connect(function(chr)
      local humanoid = chr:WaitForChild(&quot;Humanoid&quot;)
   end)
end)</code></pre><h2 id="11바인더블-이벤트">11.바인더블 이벤트</h2>
<p>바인더블 이벤트(Bindable Event)는 스크립트끼리 서로 신호를 주고받을 때 사용하는 이벤트이다.
1 : n, n : 1, n : m 개수의 스크립트로 이벤트를 주고받을 수 있다.</p>
<p>바인더블 이벤트 event에 이벤트를 추가하는 방법과 이벤트 실행 방법은 다음과 같다.</p>
<ul>
<li>이벤트 추가 : <code>event.event:(원하는 이벤트 함수)</code></li>
<li>이벤트 실행 : <code>event:Fire()</code></li>
</ul>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/283e12be-c944-4866-b52d-4ebd7e231b8e/image.png" alt="">
위의 사진과 같이 ServerStorage에 babo라는 이름의 바인더블 이벤트를 추가하고, Workspace에 A와 B 스크립트가 있을 때, A스크립트에서 B스크립트의 이벤트를 발생시키는 법은 다음과 같다.</p>
<pre><code>// A 스크립트(이벤트 실행)
local event = game.ServerStorage.babo
event:Fire()</code></pre><pre><code>// B 스크립트(이벤트 추가)
local event = game.ServerStorage.babo
event.Event:Connect(function()
   (추가할 기능)
end)</code></pre><p>바인더블 이벤트는 다른 이벤트와 마찬가지로 매개변수를 추가할 수 있다.
아래 예시는 매개변수로 휴머노이드 파트를 전송해 해당 휴머노이드를 죽이는 이벤트 바인딩이다.</p>
<pre><code>// A 스크립트(이벤트 실행)
local event = game.ServerStorage.babo
script.Parent.Touched:Connect(function(hit)
   local humanoid = hit.Parent:FindFirstChild(&quot;Humanoid&quot;)
   if humanoid then
      event:Fire(humanoid)
   end
end)</code></pre><pre><code>// B 스크립트(이벤트 추가)
local event = game.ServerStorage.babo
event.Event:Connect(function(humnoid)
   humnoid.Health =0
end)</code></pre><p>바인더블 이벤트의 매개변수에 문자로 인덱싱된 테이블과 메타테이블은 보낼 수 없다.</p>
<pre><code>tal = {}          // 테이블
tal[1] = &quot;aaa&quot;    // 숫자로 인덱싱된 테이블(매개변수 가능)
tal[&quot;a&quot;] = &quot;aaa&quot;  // 문자로 인덱싱된 테이블(매개변수 불가능)</code></pre><p>바인더블 이벤트는 추후 배울 바인더블 펑션과 다르게 값 반환(return)을 할 수 없다.
또한 바인더블 이벤트는 오직 서버-서버 스크립트에서만 사용할 수 있다.</p>
<hr>
<h2 id="12cframe-pivotto">12.CFrame, PivotTo()</h2>
<p>CFrame은 Position과 같이 파트 속성이지만 오로지 스크립트로만 조작할 수 있는 속성이다.
Position은 위치만(유니티의 Transform.Location)맞춰준다면 CFrame은 위치와 회전까지 동일하게 맞춰준다.</p>
<pre><code>// Part1의 위치만 Part2의 위치에 맞춰진다
workspace.Part1.Position = workspace.Part2.Position
// Part1의 위치와 회전이 Part2의 위치와 회전에 맞춰진다
workspace.Part1.CFrame = workspace.Part2.CFrame</code></pre><p>모델을 특정 Position으로 이동시킬 때 <code>MoveTo()</code>함수를 사용한 것처럼 <code>PivotTo()</code>함수를 이용해 특정 위치와 회전으로 이동시킬 수 있다.
이 때 <code>PivotTo()</code>함수는 <code>MoveTo()</code>함수와 다르게 충돌을 무시하고 원하는 자리로 이동시켜준다.</p>
<pre><code>// Position 이동
workspace.Model:MoveTo(workspace.Part1.Position)
// CFrame 이동
workspace.Model:PivotTo(workspace.Part1.CFrame)</code></pre><p>CFrame은 Position처럼 덧셈연산이 가능하고 추가로 곱셈연산또한 가능하다</p>
<pre><code>// 월드좌표를 기준으로 위로 10만큼 떨어진 곳으로 이동
workspace.Part1.Position = workspace.Part2.Position + Vector3.new(0,10,0)
// 월드좌표를 기준으로 회전을 유지한 채 위로 10만큼 떨어진 곳으로 이동
workspace.Part1.CFrame = workspace.Part2.CFrame + Vector3.new(0,10,0)
// Part2가 바라보는 방향을 기준으로 회전을 유지한 채 위로 10만큼 떨어진 곳으로 이동
workspace.Part1.CFrame = workspace.Part2.CFrame * CFrame.new(0,10,0)
// 모델이 바라보는 방향을 기준으로 회전을 유지한 채 위로 10만큼 떨어진 곳으로 이동
workspace.Model:PivotTo(workspace.Model:GetPivot() * CFrame.new(0,10,0))</code></pre><hr>
<h2 id="13리모트-이벤트remote-event">13.리모트 이벤트(Remote Event)</h2>
<p>리모트 이벤트는 로컬 스크립트와 서버 스크립트를 이어주는 매개체 역할을 한다.
리모트 이벤트는 로컬과 서버 스크립트 모두 접근할 수 있는 &#39;ReplicatedStorage&#39;에 추가한다.
<img src="https://velog.velcdn.com/images/_hoya_/post/58cc416c-0855-46dc-b727-4f806d58bf4b/image.png" alt=""></p>
<p>StarterPlayerScripts에 있는 로컬 스크립트에서 Workspace에 있는 Part의 색깔을 바꿔주는 스크립트는 다음과 같이 작성할 수 있다.</p>
<pre><code>// 로컬 스크립트
// 키 바인딩 서비스 불러오기
local contextActionService = game:GetService(&quot;ContextActionService&quot;)
// 리모트 이벤트 불러오기
local RemoteEvent = game.ReplicatedStorage:WaitForChild(&quot;ColorEvent&quot;)

// 리모트 이벤트 호출 함수 생성
function RPressed(actionName,  inputState, inputObject)
   if inputState == Enum.UserInputState.Begin then
      RemoteEvent:FireServer()
   end 
end

// 리모트 이벤트 호출 함수를 키에 바인드
contextActionService:BindAction(&quot;RPress&quot;, RPressed, true, Enum.KeyCode.R) </code></pre><pre><code>// 서버 스크립트
// 리모트 이벤트 불러오기
local remoteEvent = game.ReplicatedStorage.ColorEvent

// 리모트 이벤트에 기능 추가
remoteEvent.OnServerEvent:Connect(function()
   workspace.Part.BrickColor = BrickColor.Random()
end)</code></pre><p>로컬 스크립트에서는 <code>RemoteEvent:FireServer()</code>를 이용해 리모트 이벤트를 발동시켜 서버 스크립트에 신호를 줄 수 있다.
서버 스크립트에서는 신호를 받으면 <code>remoteEvent.OnServerEvent</code>에 바인딩 혹은 선언된 함수나 기능을 수행한다.</p>
<p>리모트 이벤트 또한 매개변수를 보낼 수 있다. 다만 <code>OnServerEvent</code>의 첫 번째 매개변수는 항상 이벤트를 보낸 로컬 플레이어 개체이다.</p>
<p>로컬 스크립트에서 문자열 매개변수를 보내 특정 문자에 따라 서버에 존재하는 파트의 색을 지정해 변경하는 스크립트 예시는 다음과 같다.</p>
<pre><code>// 로컬 스크립트
local contextActionService = game:GetService(&quot;ContextActionService&quot;)
local RemoteEvent = game.ReplicatedStorage:WaitForChild(&quot;ColorEvent&quot;)

function RPressed(actionName,  inputState, inputObject)
 if inputState == Enum.UserInputState.Begin then
  RemoteEvent:FireServer(&quot;R&quot;)
 end 
end
function GPressed(actionName,  inputState, inputObject)
 if inputState == Enum.UserInputState.Begin then
  RemoteEvent:FireServer(&quot;G&quot;)
 end 
end
contextActionService:BindAction(&quot;RPress&quot;, RPressed, true, Enum.KeyCode.R) 
contextActionService:BindAction(&quot;GPress&quot;, GPressed, true, Enum.KeyCode.G) </code></pre><pre><code>// 서버 스크립트
local remoteEvent = game.ReplicatedStorage.ColorEvent

remoteEvent.OnServerEvent:Connect(function(plr, key)
 if key == &quot;R&quot; then
  workspace.Part.BrickColor = BrickColor.Red()
 elseif key == &quot;G&quot; then
  workspace.Part.BrickColor = BrickColor.Green()
 end 
end)</code></pre><p>리모트 이벤트의 매개변수로는 바인더블 이벤트와 마찬가지로 문자로 인덱싱된 테이블과 메타테이블은 보낼 수 없다.
또한 리모트 이벤트는 <code>return</code>을 사용해 값을 반환할 수는 있지만, 해당 값이 로컬 스크립트에 전송되지는 않는다.</p>
<hr>
<h2 id="14팁메모-검색-기능">14.팁(메모, 검색 기능)</h2>
<h3 id="주석">주석</h3>
<p>Lua스크립트의 한 줄 주석은 <code>-- (주석글)</code>으로 작성한다.
Lua스크립트의 여러줄 주석은 <code>--[[ (주석글) ]]</code>으로 작성한다</p>
<p>&#39;ctrl&#39; + &#39;/&#39; 키보드 단축키로 선택한 스크립트 라인을 즉시 주석처리할 수 있다.</p>
<h3 id="검색">검색</h3>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/ab4e0bc5-85f9-4157-8a44-2455c3b031d0/image.png" alt="">
위의 사진과 같이 스크립트의 특정 단어들을 찾거나 바꿀 수 있게 하는 기능을 제공한다.
찾기의 키보드 단축키는 &#39;ctrl&#39; + &#39;F&#39;, 바꾸기의 키보드 단축키는 &#39;ctrl&#39; + &#39;H&#39;이다.</p>
<p>또한 &#39;ctrl&#39; + &#39;shift&#39; + &#39;F&#39;를 이용해 모두 찾기/바꾸기를 수행할 수 있다.</p>
<hr>
<h2 id="15getserveice-debris">15.GetServeice(), Debris</h2>
<h3 id="getserveice">GetServeice()</h3>
<p>만약 게임의 Players 개체에 접근하려 하면 다음과 같은 방법으로 접근할 수 있다.</p>
<ul>
<li>game.Players</li>
<li>game:GetService(&quot;Players&quot;)</li>
</ul>
<p>이 때 <code>game:GetService()</code>함수를 사용해 개체에 접근하면, 탐색기에 보이지 않는 개체들까지 접근할 수 있다. 이들을 &#39;서비스&#39;라 부른다.
탐색기에 보이지 않지만 <code>game:GetService()</code>함수를 사용해 접근하는 개체들의 예시는 다음과 같다.</p>
<ul>
<li><code>game:GetService(&quot;MarketPlaceService&quot;)</code> : 게임패스 판매 등 수익창출 관련 개체</li>
<li><code>game:GetService(&quot;ContextActionService&quot;)</code> : 마우스/키보드 등 플레이어 입력 관련 개체</li>
</ul>
<h3 id="debris">Debris</h3>
<p>Debris는 &#39;파편&#39;이라는 뜻으로 게임내 개체들을 제거해주는 서비스이다.
<code>Debris:AddItem(삭제할 개체, 시간)</code>으로 사용한다.
<code>workspace.Part</code>개체를 삭제하는 스크립트 예시는 다음과 같다..</p>
<pre><code>local debris = game:GetService(&quot;Debris&quot;)
debris:AddItem(workspace.Part)</code></pre><p>Debris 서비스를 이용해 생성 2초 후에 제거되는 파트 생성 스크립트 예시는 다음과 같다.</p>
<pre><code>local part = game.ServerStorage.Part
local debris = game:GetService(&quot;Debris&quot;)
for i=1, 50 do 
   local clone = part:Clone()
   clone.Parent = workspace
   clone.BrickColor = BrickColor.Black()
   debris:AddItem(clone, 2)
   wait()
end</code></pre><hr>
<h2 id="16간이-조건문-삼항연산자">16.간이 조건문, 삼항연산자</h2>
<h3 id="간이-조건문">간이 조건문</h3>
<p><code>local 변수 = a or b</code>는 a에 값이 선언되어 있을 시 변수에 a의 값을, 아닐 시 b의 값을 추가하는 간이 조건문이다.</p>
<p>workspace에 파트가 존재할 시 해당 파트를 변수에 할당하고, 파트가 존재하지 않으면 새 파트를 생성하는 간이 조건문 예시는 다음과 같다.</p>
<pre><code>local Part = workspace:FindFirstChild(&quot;Part&quot;) or Instance.new(&quot;Part&quot;, workspace)</code></pre><p>로컬 스크립트에서 워크스페이스에 플레이어 캐릭터가 존재할 시 해당 캐릭터를 변수에 할당하고, 존재하지 않으면 플레이어 캐릭터가 생성될 때까지 기다리는 간이 조건문 예시는 다음과 같다.</p>
<pre><code>local Player = game.Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()

-- if-else문으로 작성할 시의 스크립트
local Char
if Player.Character then
 Char = Player.Character
else
 Char = Player.CharacterAdded:Wait()
end</code></pre><h3 id="삼항연산자">삼항연산자</h3>
<p><code>local 변수 = 조건 and a or b</code>의 삼항연산자를 사용할 수 있다.
이 때 조건이 true이면 a를 , false이면 b를 변수로 지정하게 된다.</p>
<hr>
<h2 id="17break로-반복문-중단시키기">17.break로 반복문 중단시키기</h2>
<p>break를 이용해 for반복문 혹은 while반복문 중간에 특정 조건을 만족할 시 반복문을 중단 및 탈출할 수 있다.</p>
<p>for반복문의 break 예시는 다음과 같다.</p>
<pre><code>for i=1, 10 do
   wait(0.3)
   print(i)
   if i ==5 then
      break
   end
end</code></pre><p>while반복문의 break 예시는 다음과 같다.</p>
<pre><code>while true do
   wait()
   workspace.Part.BrickColor = BrickColor.Random()
   if workspace.Part.BrickColor == BrickColor.new(&quot;Really red&quot;) then
      break
   end
end</code></pre><hr>
<h2 id="18cframe-angles-각도-회전">18.CFrame Angles 각도 회전</h2>
<p>오브젝트를 원하는 각도로 회전시키기 위해서는 <code>CFrame.Angles()</code>를 사용한다.</p>
<pre><code>-- 오브젝트를 (1, 1, 1)각도로 회전
workspace.Part.CFrame = CFrame.Angles(1,1,1)
-- 오브젝트를 (5, 5, 5)위치로 이동시킨 후 (1, 1, 1)각도로 회전
workspace.Part.CFrame = CFrame.new(5,5,5) * CFrame.Angles(1,1,1)</code></pre><p><code>CFrame.Angles()</code>는 360도 기준이 아닌 라디안 기준의 값을 입력해야 한다.
따라서 오브젝트를 x축으로 30도 회전되게 설정하기 위해서는 다음과 같이 작성해야 한다.</p>
<pre><code>workspace.Part.CFrame = CFrame.new(5,5,5) * CFrame.Angles(math.rad(30),0,0)</code></pre><p>스크립트를 반복해서 실행할 때마다 각도 회전을 추가하기 위해서는 다음 예시와 같이 현재의 CFrame값에 원하는 각도를 곱해주면 된다.</p>
<pre><code>workspace.Part.CFrame = workspace.Part.CFrame * CFrame.Angles(math.rad(30),0,0)</code></pre><p>스크립트를 실행할 때마다 개체의 위치가 변경되게 하고 싶지 않으면, 개체의 현재 위치값만 가져와 다시 대입해 주면 된다.</p>
<pre><code>workspace.Part.CFrame = CFrame.new(workspace.Part.CFrame.Position) * CFrame.Angles(math.rad(30),0,0)</code></pre><hr>
<h2 id="19math-모듈">19.math 모듈</h2>
<p><code>math</code>는 Lua의 수학 모듈로 여러 함수들이 내장되어 있다.
그 중에 입력 범위 내 무작위 값을 출력하는 <code>math.random</code>함수 또한 존재한다.</p>
<pre><code>local n = math.random(1, 100) -- 1~100중 무작위 값이 n에 지정된다.</code></pre><p>&#39;매우 큰 수&#39;를 뜻하는 <code>math.huge</code>값이 존재한다. 주로 <code>wait(math.huge)</code>처럼 작성해 스크립트를 무한대로 기다리게 하는 데에 사용한다.
혹은 오브젝트의 속성 중에 <code>math.huge</code>값을 도입할 수 있는 속성 또한 있다. 이들은 속성창에 &#39;inf&#39;라 표시된다.</p>
<p>원주율을 가리키는 <code>math.pi</code>가 존재한다. <code>math.pi</code>는 3.1415926535898까지 표시된다.
호, 구체 등 원과 관련된 계산을 할 때 자주 사용한다.</p>
<p>선택한 값들 중 가장 큰 값, 혹은 가장 작은 값을 반환해주는 <code>math.max()</code>함수와 <code>math.min</code>함수 또한 존재한다.</p>
<pre><code>print(math.min(1, 5, 23, -26, 3, 34, 15, 5 ,1)) -- -26 출력
print(math.max(1, 5, 23, -26, 3, 34, 15, 5 ,1)) -- 34 출력</code></pre><p>소수점 자릿수를 반올림해 정수로 반환해주는 <code>math.round()</code>함수도 존재한다.
올림 함수는 <code>math.ceil()</code>, 내림 함수는 <code>math.floor()</code>이다.</p>
<hr>
<h2 id="20배열-array-table">20.배열, array, table</h2>
<h3 id="배열의-기초">배열의 기초</h3>
<p>Lua의 배열은 다음과 같이 선언할 수 있다.</p>
<pre><code>local array = {}</code></pre><p>Lua의 배열은 한 배열 안에 여러 자료형의 값을 넣을 수 있다. 또한 배열 번호가 0이 아닌 1부터 시작한다.
배열의 수정은 C언어 계열과 동일하게 할 수 있다.</p>
<pre><code>local array = {1234, 1222, &quot;string&quot;, true}
print(array[2]) -- 1222출력
array[2] = 4366
print(array[2]) -- 4366출력</code></pre><p>배열을 저장한 변수에 <code>#</code>을 붙이면 그 배열의 길이를 알 수 있다.</p>
<pre><code>local array = {1234, 1222, &quot;string&quot;, true}
print(#local) -- 4 출력</code></pre><p><code>table.insert(배열, 배열 내 위치, 저장할 값)</code>함수를 이용해 배열의 중간에 값을 추가할 수 있다.
이를 응용해 배열 맨 뒷자리에 값을 추가하는 스크립트 또한 작성할 수 있다.</p>
<pre><code>-- 배열 중간에 값 추가하기
local array = {1234, &quot;string&quot;, true}
table.insert(array, 2, 4355) -- 추가할 배열, 추가할 위치, 추가할 값 순으로 입력

-- 배열 맨 뒷자리에 값 추가하기
local array = {1234, &quot;string&quot;, true}
table.insert(array, #array + 1, 4355) -- 맨 뒷자리에 추가
table.insert(array, 4355) -- 맨 뒷자리에 추가하는 경우 위치 생략 가능</code></pre><p><code>table.remove(배열, 배열 내 위치)</code>함수를 이용해 배열의 특정 위치에 있는 값을 삭제할 수 있다.
또한 <code>nil</code>을 이용해 배열 내 특정 위치의 값을 공백으로 남길 수 있다.</p>
<pre><code>-- 배열 안의 값 삭제하기
local array = {1234, &quot;string&quot;, true}
table.remove(array, 2)

-- 배열 안의 값 삭제하고 그 자리 공석으로 남기기
local array = {1234, &quot;string&quot;, true}
array[2] = nil</code></pre><h3 id="배열-심화">배열 심화</h3>
<p>Lua의 배열은 배열의 범위를 넘어가는 값을 지정하고 출력을 요구하면 <code>nil</code>값을 출력한다.</p>
<p>배열 안에 배열을 저장할 수 있다.</p>
<pre><code>local array = {1234}
local array2 = {array}
print(array2[1][1]) -- 1234 출력</code></pre><p>배열 안에 함수를 추가할 수 있다.</p>
<pre><code>local array = {function()
 print(&quot;aaaaa&quot;)
end}
array[1]() -- aaaaa 출력</code></pre><hr>
<h2 id="21getchildren-플레이어-목록-구하기">21.GetChildren(), 플레이어 목록 구하기</h2>
<p><code>GetChildren()</code>함수는 개체의 자식 개체들을 통합해서 배열로 반환해준다.
이때 스크립트, 모듈 등 모든 자식 개체들을 종류에 관계없이 반환해준다.</p>
<pre><code>local model = script.Parent
local parts = model:GetChildren()</code></pre><p><code>GetDecendent()</code>함수는 2중, 3중으로 자식에 속해있는 자식 개체들까지 통합해서 배열로 반환해준다.</p>
<pre><code>local model = script.Parent
local parts = model:GetDescendants()</code></pre><p>Players 개체는 <code>GetChildren</code>함수보단 <code>GetPlayers()</code>함수를 통해 자식으로 소환되어 있는 캐릭터 개체를 구하는 것이 낫다.</p>
<pre><code>local parts = game.Players:GetPlayers()</code></pre><p><code>GetChildren()</code>계열의 함수는 서로 다른 종류의 모든 자식 개체를 가져오기 때문에 반복문을 사용해 개체의 변수값을 일괄적으로 변경할 시 조건문을 활용해 각각의 자식 개체가 어떠한 개체 종류인지 확인해야 한다.
이 때 모든 개체에 사용할 수 있는 <code>IsA()</code>함수를 이용해 개체가 어떤 파트를 가지고 있는지 확인할 수 있다.</p>
<pre><code>local model = script.Parent
local parts = model:GetChildren()

for i=1, #parts do
   if parts[i]:IsA(&quot;BasePart&quot;) then
      parts[i].CanCollide = false
      parts[i].BrickColor = BrickColor.Random()  
   end
end</code></pre><p>Lua에서 C#의 &#39;foreach&#39;와 유사한 반복문을 사용할 수 있다.
위의 조건 반복 예시를 Lua의 foreach 형식으로 변경하면 다음과 같다.</p>
<pre><code>for i, v in pairs(parts)do
   if v:IsA(&quot;BasePart&quot;) then
      v.CanCollide = false
      v.BrickColor = BrickColor.Random()  
   end
end</code></pre><hr>
<h2 id="22테이블-딕셔너리">22.테이블, 딕셔너리</h2>
<p>딕셔너리는 다음과 같이 선언 및 접근할 수 있다.</p>
<pre><code>local dictionary = {
 aaa = &quot;red&quot;;
 bb = &quot;blue&quot;;
 c = &quot;green&quot;;
}

print(dictionary[&quot;aaa&quot;]) -- &quot;red&quot; 출력
print(dictionary.aaa) -- &quot;red&quot; 출력</code></pre><p>딕셔너리의 키에는 모든 종류의 자료형이 들어갈 수 있다. 이미 변수로 선언된 값 또한 들어갈 수 있다. 단 <code>nil</code>값은 불가능하다.</p>
<pre><code>local s= true
local dictionary = {
 aaa = &quot;red&quot;;
 bb = &quot;blue&quot;;
 c = &quot;green&quot;;
 [&quot;string&quot;] = &quot;blue&quot;;
 [s] = &quot;yellow&quot;
}</code></pre><p>배열과 딕셔너리를 섞어 선언할 수도 있다.</p>
<pre><code>local mixed = {&quot;red&quot;; &#39; blue&#39;; &#39;green&#39;;
 aaa = &quot;red&quot;;
 bb = &quot;blue&quot;;
 c = &quot;green&quot;;
}</code></pre><p>배열의 foreach문은 다음과 같이 사용한다.</p>
<pre><code>for i, v in ipairs(array)do
 print(i, v)
end</code></pre><p>딕셔너리 혹은 혼합 배열의 foreach문은 다음과 같이 사용한다.</p>
<pre><code>for i, v in pairs(dictionary)do
 print(i, v)
end</code></pre><p><code>ipairs()</code>는 오직 순수 배열에만 작동하는 함수이기 때문에 딕셔너리에 적용할 시 아무 것도 반환하지 않는다.
또한 배열 중간에 <code>nil</code>값이 있는 경우에 <code>ipairs()</code>는 <code>nil</code>값 이후의 값들은 반환을 하지 않는다.
하지만 <code>ipairs()</code>가 <code>pairs()</code>보다 효율적인 함수이다.</p>
<p>딕셔너리에는 배열에서 쓰던 <code>remove</code>, <code>insert</code>, <code>#</code>을 사용할 수 없다.</p>
<hr>
<h2 id="23전역변수-value-개체">23.전역변수? Value 개체</h2>
<p>로블록스에는 다음과 같이 생성할 수 있는 Value 개체가 있다.
<img src="https://velog.velcdn.com/images/_hoya_/post/a2a91289-ac7f-4bb5-a619-ef22f4265b11/image.png" alt=""></p>
<p>로블록스에서는 이와 같이 Value개체에 값을 저장하고, 이 값을 스크립트들이 접근하는 방식으로 전역 변수처럼 사용한다.
Value 개체를 스크립트에서 접근 및 사용하는 방식은 다른 일반적인 개체들과 동일하다.</p>
<pre><code>---스크립트A
local numValue = workspace.Num
numValue.Value = 19</code></pre><p>또한 이렇게 설정한 값을 다른 스크립트에서 접근할 수 있다.</p>
<pre><code>---스크립트B
wait(1)
print(workspace.Num.Value)</code></pre><p>Value 개체 또한 다른 개체처럼 이벤트를 사용할 수 있다.</p>
<pre><code>local numValue = workspace.Num

-- numValue 개체 내 value값이 변경될 때 호출됨
numValue.Changed:Connect(function(num)
   print(num)
end)</code></pre><hr>
<h2 id="24cframelookat-특정-파트-바라보게-하기">24.CFrame.lookAt(), 특정 파트 바라보게 하기</h2>
<p><code>CFrame.lookAt()</code>함수로 해당 오브젝트를 특정 오브젝트의 방향으로 회전시킬 수 있다.</p>
<pre><code>local part = workspace.Part
local redPart = workspace.RedPart

part.CFrame = CFrame.lookAt(part.Position, redPart.Position) </code></pre><p>이를 이용해 나만을 바라보는 npc 머리 스크립트는 다음과 같이 작성할 수 있다.</p>
<pre><code>local npcHead = workspace:WaitForChild(&quot;Head&quot;)
local MyHead = script.Parent:WaitForChild(&quot;Head&quot;)

while wait() do
   npcHead.CFrame = CFrame.lookAt(npcHead.Position, MyHead.Position)
end</code></pre><p>CFrame.Rotation은 파트의 회전각이다.
이를 이용해 redPart가 part와 동일한 회전도가 되게 하는 방식은 다음과 같다.</p>
<pre><code>local part = workspace.Part
local redPart = workspace.RedPart

redPart.CFrame = CFrame.new(redPart.Position) * part.CFrame.Rotation</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[로블록스 루아 스크립트 기초]]></title>
            <link>https://velog.io/@_hoya_/Roblox-Lua-Basic</link>
            <guid>https://velog.io/@_hoya_/Roblox-Lua-Basic</guid>
            <pubDate>Mon, 01 Apr 2024 08:45:48 GMT</pubDate>
            <description><![CDATA[<p><a href="https://youtube.com/playlist?list=PL6Zm_z30gEKQPAGtNNd6fiB9jjaQDimNL&amp;si=4pNhaIE0cmaTDEY2">강의 재생목록</a></p>
<p>C계열 언어와 유니티를 익힌 사람을 기준으로 작성했습니다.</p>
<hr>
<h2 id="01스크립트-시작하기">01.스크립트 시작하기</h2>
<h3 id="스크립트-창-열기">스크립트 창 열기</h3>
<p>로블록스 스크립팅에 사용하는 유용한 탭은 상단바의 <code>보기</code>탭에서 선택해 열 수 있다.
탭의 종류는 다음과 같다.</p>
<ul>
<li>탐색기</li>
<li>속성</li>
<li>출력</li>
<li>명령 모음</li>
</ul>
<p>탭을 오픈한 후 상단바의 <code>모델</code>탭에서 스크립트를 오픈한다.</p>
<h3 id="print-스크립트-실행해보기">print 스크립트 실행해보기</h3>
<p><code>print</code>는 &#39;출력&#39;창에 원하는 문자열 및 숫자를 출력해주는 스크립트이다.
<code>print</code>는 다음과 같이 사용할 수 있다.</p>
<pre><code>print(&quot;Hello world!&quot;)
print(&quot;한국어&quot;)
print(1234567890)</code></pre><p>스크립트를 작성한 후 상단의 <code>실행</code> 버튼이 아니라 <code>실행</code>버튼 아래 화살표를 통해 나오는 <code>Run</code>버튼을 눌러 스크립트를 테스트한다.
<code>Run</code>버튼은 플레이어 스폰을 시키지 않는 실행 버튼이다. 주로 스크립트 테스트에 사용된다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/2e7e4922-7ae9-4694-9de4-0348642292c9/image.png" alt=""></p>
<p>만약 스크립트에 오류가 있는 채로 <code>Run</code>을 수행할 시 다음과 같이 오류 메세지가 출력된다. 붉은 오류 메세지를 출력하여 오류가 발생한 스크립트 부분으로 이동할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/f723fad2-aec9-432c-91ef-2985c7f733e2/image.png" alt=""></p>
<hr>
<h2 id="02탐색기-개체-부모와-자식">02.탐색기, 개체, 부모와 자식</h2>
<p>탐색기(Explorer)는 맵 위에 존재하는 모델들, 그 모델들의 파트들, 맵 상으로 선택할 수 없는 스크립트, 라이트 등을 모두 포함하여 탐색 및 정리를 할 수 있게 해주는 창이다.</p>
<p>탐색기 안에서 하나하나 보여지는 물건들을 개체(Object)라고 한다.</p>
<p>스크립트에 대한 부모와 자식 개체는 탐색창에서 다음과 같이 표현된다.
<img src="https://velog.velcdn.com/images/_hoya_/post/8c722840-72d3-48f8-8418-3634bbe450d7/image.png" alt=""></p>
<p>스크립트에서 부모와 자식 개체는 다음과 같이 접근할 수 있다.</p>
<pre><code>// script는 스크립트 개체 자기 자신을 의미한다.

// 부모 개체에 대한 접근은 Parent라는 고유 예약어를 사용한다.
print(script.Parent) 
// 자식 개체는 해당 개체의 이름을 통해 접근한다.
print(script.Child) 
// 이와 같이 부모-다른 자식 접근을 수행할 수 있다.
print(script.Parent.Camera)</code></pre><p>부모자식 접근을 제외하고 다음과 같이 지정된 개체에는 직접 접근할 수 있다.</p>
<pre><code>print(workspace) // 워크스페이스 접근
print(game) // 게임(최상단) 접근
print(game.Workspace) // 게임에 속한 워크스페이스 접근
print(game.Players) // 게임에 속한 플레이어 접근
print(game.Lighting.Sky) // 게임에 속한 라이팅의 스카이 접근</code></pre><hr>
<h2 id="03개체-탐색시-주의사항">03.개체 탐색시 주의사항</h2>
<p>이름으로 개체를 찾을 때 스크립트로 찾는 개체가 다른 개체랑 겹치면 안된다.
즉 동일한 부모를 가진 개체들은 모두 다른 이름을 가져야 한다.</p>
<p>또한 다음과 같은 이름을 가진 자식들은 접근할 때 대괄호<code>[&quot; &quot;]</code>를 이용해 이름을 작성해야 한다.</p>
<ul>
<li>이름이 숫자로 시작하는 경우</li>
<li>이름 중간에 띄어쓰기가 있는 경우</li>
<li>이름이 알파벳을 아닌 다른 언어의 문자(한글, 한자 등)이 포함된 경우</li>
<li>이름에 특수문자(!@#$%^&amp; 등)이 포함된 경우</li>
</ul>
<pre><code>print(game.MaterialService[&quot;CarPaint_A&quot;])</code></pre><hr>
<h2 id="04속성-속성의-접근">04.속성, 속성의 접근</h2>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/337a10cf-87d5-4728-acd9-992cd9f3b2e1/image.png" alt=""></p>
<p>어떤 개체를 선택했을 때 속성 창에 그 개체의 속성(Properties)들이 나타난다.</p>
<p>스크립트상에서 속성에 접근하기 위해서는 개체의 부모자식에 접근하듯이 해당 속성의 이름을 그대로 작성해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/c6af3944-8380-472d-b559-8dd5faff45f3/image.png" alt=""></p>
<pre><code>// 해당 스크립트가 부착된 개체의 투명도를 0으로 변경
script.Parent.Transparency = 0
// 해당 스크립트가 부착된 개체의 이름을 &quot;LFDoor&quot;로 변경
script.Parent.Name = &quot;LFDoor&quot;</code></pre><p>속성의 값을 변경할 때에는 그 속성에 알맞는 값 형식만을 사용해야 한다.</p>
<hr>
<h2 id="05boolean-enum">05.boolean, enum</h2>
<h3 id="boolean">boolean</h3>
<p>다음과 같이 속성창에 체크박스로 표시되는 속성값은 boolean이다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/31ad7653-c68f-40ee-b365-ff2115fce496/image.png" alt=""></p>
<p>boolean형식은 true와 false 형식을 갖고 있다. 스크립트 상에서 다음과 같이 변경이 가능하다.</p>
<pre><code>script.Parent.Anchored = true
script.Parent.Anchored = false</code></pre><h3 id="enum">enum</h3>
<p>다음과 같이 속성창에 드롭다운 메뉴가 표시되는 속성값은 enum이다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/d4e6521f-2851-411a-8bef-d83d0509ae5d/image.png" alt=""></p>
<p>enum형식은 스크립트 상에서 다음과 같이 변경이 가능하다.</p>
<pre><code>// enum 형식의 이름을 직접 문자열로 입력
script.Parent.Material = &quot;Brick&quot;
// enum 형식 모음집을 통해 변경
script.Parent.Material = Enum.Material.Brick</code></pre><hr>
<h2 id="06디벨로퍼-허브">06.디벨로퍼 허브</h2>
<h3 id="디벨로퍼-허브">디벨로퍼 허브</h3>
<p><a href="https://developer.roblox.com/">디벨로퍼 허브</a></p>
<p>디벨로퍼 허브에서 클래스, 속성 등에 대한 api를 검색하고 조사할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/7b39280d-f6f0-4c93-885a-ce931702209f/image.png" alt=""></p>
<h3 id="추가">추가</h3>
<p>다음과 같이 스크립트의 부모를 변경할 수 있다.</p>
<pre><code>script.Parent = workspace</code></pre><hr>
<h2 id="07커맨드-바-색-변경">07.커맨드 바, 색 변경</h2>
<h3 id="커맨드-바">커맨드 바</h3>
<p>커맨드 바는 스크립트 한 줄을 <code>run</code>없이 즉시 실행하고 싶을 때 사용한다.
다만 탐색 시작지점을 <code>workspace</code>로 해야 한다.</p>
<h3 id="색-변경">색 변경</h3>
<p>개체의 색은 다음과 같이 변경할 수 있다.</p>
<p><a href="https://create.roblox.com/docs/ko-kr/reference/engine/datatypes/BrickColor">색 변경</a></p>
<hr>
<h2 id="08사칙연산-제곱-나머지">08.사칙연산, 제곱, 나머지</h2>
<p>Lua스크립트의 사칙연산 기호는 각각 <code>+</code>, <code>-</code>, <code>*</code>, <code>/</code>이다.
Lua스크립트의 제곱 및 나머지 기호는 각각 <code>^</code>, <code>%</code>이다.</p>
<p>Lua스크립트는 다른 언어와 마찬가지로 괄호 우선 연산을 지원한다.</p>
<pre><code>print((2 + 2) * 2 / 2 - 2)
print(2 ^ 2)
print(21 % 4)</code></pre><hr>
<h2 id="09숫자-비교하기-등호">09.숫자 비교하기, 등호</h2>
<p>Lua스크립트의 등호 종류는 다음과 같다</p>
<ul>
<li><code>==</code> : 동일 여부</li>
<li><code>~=</code> : 비동일 여부</li>
<li><code>&lt;</code>, <code>&gt;</code> : 작다, 크다</li>
<li><code>&lt;=</code>, <code>&gt;=</code> : 작거나 같다, 크거나 같다</li>
</ul>
<pre><code>print(2 + 2 * 2 == 6) // true 출력
print(2 + 2 * 2 == 7) // false 출력</code></pre><hr>
<h2 id="10조건문-if-then-end">10.조건문 if-then-end</h2>
<p>Lua스크립트의 조건문 기본은 다음과 같다.</p>
<pre><code>if (조건식) then
    실행문
end
...</code></pre><p>조건식이 true일 때에 실행문이 실행되고 false일 때에는 실행문이 실행되지 않는다.
nil 또한 조건문에서는 false로 판단한다.</p>
<hr>
<h2 id="11조건문-notelseelseif">11.조건문 not/else/elseif</h2>
<h3 id="not">not</h3>
<p>어떠한 식에 반대되는 결과를 출력시키고 싶으면 <code>not</code>을 사용하면 된다.</p>
<pre><code>print(not true) // false 출력</code></pre><p>Lua 조건문의 조건식을 반전시키고 싶으면 <code>not</code>을 사용하면 된다.
<code>not</code>은 사칙연산 및 비교식보다 계산 우선순위가 위이므로 조건식을 괄호로 감싸지 않으면 계산식에 오류가 생길 수 있다.</p>
<pre><code>if not (조건식) then
    실행문
end
...</code></pre><h3 id="elseelseif">else/elseif</h3>
<p>Lua 조건문에 추가적인 조건을 설정하고 싶으면 다음과 같이 수행할 수 있다.
조건문의 흐름은 C언어 계열과 동일하다.</p>
<pre><code>if (조건식1) then
    실행문1
elseif (조건식2) then
    실행문2
else
    실행문3
end
...</code></pre><hr>
<h2 id="12nilandor-계산-우선순위">12.nil/and/or, 계산 우선순위</h2>
<h3 id="nil">nil</h3>
<p>Lua에서 &#39;값이 없음&#39;을 <code>nil</code>로 표현한다. C언어의 <code>null</code>과 동일한 개념이다.</p>
<h3 id="andor">and/or</h3>
<p>Lua에서 조건문 등에 조건을 중첩할 때 C언어와 마찬가지로 <code>and</code>, <code>or</code>을 사용할 수 있다.</p>
<ul>
<li><code>and</code> : ~하고</li>
<li><code>or</code> : ~하거나</li>
</ul>
<pre><code>if (조건문1) and (조건문2) then
    (실행문)
end

if (조건문1) or (조건문2) then
    (실행문)
end</code></pre><h3 id="계산-우선순위">계산 우선순위</h3>
<p>(위쪽일수록 먼저 계산됨)
(같은 층에 있으면 우선순위 같은 거)</p>
<pre><code>^
not, #, -(음수)
*, /, %
+, - 
..
~=, ==, 부등호
and
or</code></pre><hr>
<h2 id="13파트-크기-vector31">13.파트 크기, Vector3(1)</h2>
<h3 id="파트">파트</h3>
<p>로블록스는 모든 오브젝트의 크기를 직육면체 형태의 파트로 정한다.
구, 실린더 등의 비 직육면체 오브젝트도 크기는 직육면체 형태의 파트로 정해진다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/c6ec6eba-8718-4cb0-a2b5-ffc6d31e176e/image.png" alt=""></p>
<p>파트의 방향은 다음과 같이 정해진다.</p>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/d8ebfbce-4a71-4636-9103-184c4e34e3bf/image.png" alt=""></p>
<h3 id="vector31">Vector3(1)</h3>
<p>파트의 자료형은 Vector3이며 파트의 크기는 Vector3형식으로 설정할 수 있다.</p>
<pre><code>workspace.Part.Size = Vector3.new(4, 1, 2) * 2
workspace.Part.Size = workspace.Part.Size * 2
workspace.Part.Size = workspace.Part.Size + Vector3.new(4, 1, 2)</code></pre><hr>
<h2 id="14repeat-반복문-vector32">14.repeat 반복문, Vector3(2)</h2>
<h3 id="repeat">repeat</h3>
<p>어떠한 조건이 맞을 때까지 수행하는 반복문을 사용하고 싶을 때 <code>repeat</code>반복문을 사용한다.</p>
<pre><code>repeat
    (실행문)
until (조건문)</code></pre><p>실행문 문단에 <code>wait()</code>함수를 넣으면 조건이 맞을 때까지 가만히 기다리는 반복문이 된다.
혹은 다음과 같이 특정 조건에 맞게끔 값을 변경하는 실행문을 추가할 수 있다.</p>
<pre><code>repeat
    workspace.PrimaryPart.Size = workspace.PrimaryPart.Size + Vector3.new(0, 1, 0)
until workspace.PrimaryPart.Size.Y &gt;= 10</code></pre><h3 id="vector32">Vector3(2)</h3>
<p>벡터3는 다음과 같이 소수부 또한 포함할 수 있다.</p>
<pre><code>Vector3.new(1.1, 0.3, 3.7)</code></pre><hr>
<h2 id="15while반복문-무한-반복문">15.while반복문, 무한 반복문</h2>
<p>while조건문은 어떠한 조건이 false가 될 때까지 실행하는 조건문이다.</p>
<pre><code>while (조건문) do
(실행문)
end</code></pre><p>조건문이 항상 true일 시 과부화가 될 수 있으므로 <code>wait()</code>함수를 실행문 맨 위 혹은 조건문으로 설정하여 부하를 줄인다.</p>
<pre><code>while wait() do
(실행문)
end</code></pre><hr>
<h2 id="16변수">16.변수</h2>
<p>Lua도 C언어 계열처럼 변수 선언, 할당 및 접근 작업을 수행할 수 있다.</p>
<pre><code>// 변수 선언 이전
workspace.PrimaryPart.Size = Vector3.new(0, 1, 0)

// 변수 선언
model = workspace.PrimaryPart
vecSize = Vector3.new(0, 1, 0)

// 변수 선언 이후
model.Size = vecSize</code></pre><hr>
<h2 id="17변수-주의사항-local변수">17.변수 주의사항, local변수</h2>
<h3 id="변수-주의사항">변수 주의사항</h3>
<p>변수 이름으로 사용할 수 없는 이름 종류는 다음과 같다.</p>
<ul>
<li>숫자로 시작하는 이름</li>
<li>중간에 띄어쓰기가 존재하는 이름</li>
<li>이름에 한국어나 특수문자(언더바 제외)가 존재하는 이름</li>
<li>이미 사용중인 이름 혹은 예약어로 이미 존재하는 이름</li>
</ul>
<p>Lua는 C언어 계열과 다르게 변수의 자료형을 중간에 임의로 변경할 수 있다.
다만 되도록 하나의 변수에는 하나의 자료형만 넣도록 하자.</p>
<pre><code>vecSize = Vector3.new(0, 1, 0)

// 변수 다시 선언
vecSize = 0.5</code></pre><h3 id="로컬-변수">로컬 변수</h3>
<p>변수 앞에 <code>local</code>이 붙으면 그 변수는 선언하는 변수가 존재하는 구역에서만 사용할 수 있다.</p>
<ul>
<li>스크립트 내에 존재하는 local변수는 해당 스크립트 내에서만 사용한다는 의미</li>
<li>반복문, 조건문 등의 구문 안에 존재하는 local변수는 해당 구문 안에서만 사용한다는 의미<pre><code>local var1 = 1 // 스크립트 내에서 사용
</code></pre></li>
</ul>
<p>if (조건식1) then
    local var2 = 1 // 조건문 안에서만 사용
    (실행문1)
end
while (조건식2) do
    local var3 = 1 // 반복문 안에서만 사용
end</p>
<pre><code>
구문 밖에 선언된 로컬 변수와 안에 선언된 로컬 변수는 이름이 같아도 서로 다른 변수 취급된다.
</code></pre><p>local var = 1 // 스크립트 내에서 사용</p>
<p>if (조건식1) then
    local var = 3 // 조건문 안에서만 사용
    print(var) // 3 출력
end</p>
<p>print(var) // 1 출력</p>
<pre><code>
---

## 18.for반복문

for반복문으로 반복 횟수를 지정할 수 있다.</code></pre><p>// i가 1에서 시작해 7이 될 때까지 1씩 증가하는 반복문
for i=1, 7, 1 do
    print(i, &quot;번째 반복입니다&quot;)
end</p>
<p>// i가 20에서 시작해 0이 될 때까지 2씩 감소하는 반복문
for i=20, 0, -2 do
    print(i, &quot;번째 반복입니다&quot;)
end</p>
<pre><code>위의 예시에서 i는 반복문 안에서만 사용되는 로컬 변수이다

---

## 19.파트 위치, Vector3(3)

파트 위치 변수값의 자료형 또한 Vector3이다.
로블록스 세상의 중심 위치는 Vector3(0, 0, 0)이다.

---

## 20.파트 생성

로블록스에서 스크립트를 이용해 파트를 생성하는 방법은 다음과 같다.</code></pre><p>Instance.new(&quot;파트 클래스 이름&quot;, 부모 파트)</p>
<pre><code>
생성한 파트는 유니티와 비슷하게 local변수에 할당하여 스크립트에서 임의로 조절할 수 있다.</code></pre><p>// 랜덤 색의 구체 파트를 (0, 1, 0)위치에 생성하는 함수
local part = Instance.new(&quot;Part&quot;, workspace)
part.Position = Vector3.new(0, 1, 0)
part.Shape = Enum.PartType.Ball
part.TopSurface = Enum.SurfaceType.Smooth
part.BottomSurface = Enum.SurfaceType.Smooth
part.BrickColor = BrickColor.Random()</p>
<pre><code>
---

## 21.파트 삭제와 함수

### 파트 삭제

파트는 `Destroy()`함수로 삭제할 수 있다.</code></pre><p>// 삭제할 파트 변수에 할당
local part = workspace.Model1.Part2
part:Destroy()</p>
<pre><code>
### 함수

파트에는 파트의 상태변화 혹은 작업 수행을 하게 해주는 함수가 존재한다.
함수는 파트 뒤에 콜론(:)을 붙여 선언할 수 있다.</code></pre><p>part:GetMass() // 파트의 질량을 구하는 함수
sound:Play() // 사운드를 플레이하는 함수</p>
<pre><code>
내장함수는 스크립트 내부에 저장되어 있는 함수로, 파트의 선언이 없어도 사용할 수 없다.</code></pre><p>print()
wait()</p>
<pre><code>
---

## 22.파트 복사, Clone(), ServerStorage

### ServerStorage

로블록스의 서버 스토리지(ServerStorage)는 로블록스 내 저장공간으로, 스토리지 내에서는 스크립트가 작동되지 않는다.
따라서 추후에 스폰/작동되는 파트와 스크립트들은 서버 스토리지에 저장해둔 후 오브젝트 클론을 통해 복사해와 사용할 수 있다.

### 파트 복사

서버 스토리지 내의 &#39;Part&#39;라는 이름의 파트를 워크스페이스로 복사해오려면 다음과 같이 작성하면 된다.</code></pre><p>local part = game.ServerStorage.Part
local clone = part:Clone()
clone.Parent = workspace // 워크스페이스로 옮기지 않으면 계속 ServerStorage에 존재</p>
<pre><code>
---

## 23.함수, function

Lua에서 함수는 다음과 같이 작성한다. 함수명 또한 변수명의 작명 규칙을 따른다.</code></pre><p>function 함수명()</p>
<p>end</p>
<pre><code>
&quot;start cloning part&quot;를 프린트한 후 2초 후에 파트를 클론하는 스크립트는 다음과 같이 작성할 수 있다.
</code></pre><p>local function ClonePart()
    local part = game.ServerStorage.Part
    local clone = part:Clone()
    clone.Parent = workspace
end</p>
<p>print(&quot;start cloning part&quot;)
wait(2)
ClonePart()</p>
<pre><code>
Lua는 위에서부터 아래로 스크립트를 읽으므로 함수는 무조건 위에 선언해 주어야 한다.
또한 함수에도 변수와 마찬가지로 local을 붙일 수 있으며, local을 붙이는 것이 더 효율적이다.

---

## 24.함수 매개변수와 반환

Lua의 함수는 다음과 같이 매개변수를 받을 수 있다.
매개변수는 함수 안에서만 사용할 수 있는 로컬 변수이다.
또한 return을 이용해 결과값을 반환할 수도 있다.
</code></pre><p>// part를 클론한 후 location의 자식으로 만들어 반환하는 함수
local function ClonePart(part, location)
    local clone = part:Clone()
    clone.Parent = location
    return clone
end</p>
<p>local clone = ClonePart(game.ServerStorage.Part, workspace)
clone.BrickColor = BrickColor.Random()
```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[포톤 VR 크로스플랫폼 개발 후기]]></title>
            <link>https://velog.io/@_hoya_/Photon-Crossplatform-Review</link>
            <guid>https://velog.io/@_hoya_/Photon-Crossplatform-Review</guid>
            <pubDate>Tue, 06 Feb 2024 08:26:32 GMT</pubDate>
            <description><![CDATA[<p>상용 서버인 포톤으로 모바일, PC, VR 크로스플레이를 지원하는 동영상 시청 공유 메타버스 앱을 구현하였다.
Photon을 이용한 실시간 네트워킹, Unity OpenXR을 이용한 VR앱 구현 및 크로스플랫폼 플레이어 구현을 목표로 하였다.</p>
<hr>
<h2 id="새로-학습한-부분">새로 학습한 부분</h2>
<h3 id="포톤-퓨전">포톤 퓨전</h3>
<p><a href="https://velog.io/@_hoya_/Unity-Photon-Fusion">기술 벨로그</a></p>
<p>포톤사에서 유니티 전용으로 출시한 라이브 서버이다.
공식 튜토리얼로는 간단한 연결만 수행할 수 있어 추가적인 튜토리얼을 수행하였다.
세션을 만들고, 그 세션에 참여하여 트랜스폼과 애니메이션, 각종 변수들과 RPC를 이용한 동기화를 공부하였다.</p>
<h3 id="openxr">OpenXR</h3>
<p><a href="https://velog.io/@_hoya_/Unity-VR-Start">기술 벨로그</a></p>
<p>VR/AR 표준 오픈소스인 OpenXR을 이용해 모든 VR기기에서 실행 가능한 애플리케이션을 구현할 수 있다.
OpenXR의 플레이어 시스템을 이용한 이동/카메라 움직임/텔레포트를 직접 구현하고, 유니티의 XR Interaction Toolkit을 이용해 UI 상호작용, 오브젝트 상호작용을 직접 구현하였다.</p>
<hr>
<h2 id="직접-개발한-부분">직접 개발한 부분</h2>
<h3 id="크로스플랫폼-구현">크로스플랫폼 구현</h3>
<p><a href="https://velog.io/@_hoya_/Unity-OneProject-CrossPlatform">기술 벨로그</a></p>
<p>하나의 프로젝트로 여러 개의 플랫폼에 대한 빌드를 시도할 때 수행한 작업들을 적어두었다.
멀티플랫폼을 위한 프로젝트 내부 설정과 플랫폼 체크, 플랫폼에 따른 플레이어/오브젝트/UI 소환 방법을 주로 다루었다.</p>
<h3 id="비디오-플레이어">비디오 플레이어</h3>
<p><a href="https://velog.io/@_hoya_/Unity-Video-Player">기술 벨로그</a></p>
<p>플레이어들이 앱 내에서 동영상을 시청할 수 있도록 비디오 플레이어를 구현하였다.
<code>VideoPlayer</code>컴포넌트와 UI를 조합해 재생/일시정지, 재생 속도, 볼륨 등을 조절하는 비디오 플레이어 스크립트를 구현하였다.</p>
<hr>
<h2 id="좋았던-부분">좋았던 부분</h2>
<h3 id="새로운-시도">새로운 시도</h3>
<p>요즘 새롭게 화두되고 있는 크로스플랫폼을 구현해본 것이 좋았다.
각 플랫폼의 입력 처리 및 화면 특징 등에 대해 분석하여 이에 맞게 플레이어를 구현하고, 또한 각 플랫폼의 성능에 맞춰 에셋 퀄리티를 조절하는 방식을 공부할 수 있었다.
또한 동영상 플레이어를 구현하면서 게임 외적으로 유니티를 사용하는 방법을 일부 엿볼 수 있었다.</p>
<h3 id="라이브-서버-공부">라이브 서버 공부</h3>
<p>이전에 로그인과 데이터베이스를 처리하는 웹서버와 연동한 경험은 있었지만, 멀티플레이를 위한 라이브 서버와의 연동을 처음 수행해보았다.
비록 클라이언트를 위해 만들어진 서버가 아니라 상용 라이브 서버였지만 컴포넌트 값 및 변수값들의 연동 방식을 알 수 있었다.</p>
<hr>
<h2 id="아쉬웠던-부분">아쉬웠던 부분</h2>
<h3 id="미흡한-사전지식">미흡한 사전지식</h3>
<p>XR과 라이브 서버에 대해 미흡한 상태로 개발을 시작하여 최적화가 덜 된 결과물이 나왔다.
특히 라이브 서버에 플레이어 이동 스크립트를 틱마다 업데이트하게 한 것이 제일 큰 실수였다.</p>
<hr>
<h2 id="느낀-점">느낀 점</h2>
<p>해당 프로젝트가 장기 프로젝트 형태로 오면 모든 지식을 총동원해 구현해보고 싶다.
이대로 끝내기에는 무언가 약간 아쉬운 프로젝트였다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 비디오 플레이어]]></title>
            <link>https://velog.io/@_hoya_/Unity-Video-Player</link>
            <guid>https://velog.io/@_hoya_/Unity-Video-Player</guid>
            <pubDate>Fri, 26 Jan 2024 14:15:32 GMT</pubDate>
            <description><![CDATA[<p><a href="https://youtu.be/zZ85f2kj9Xs">시연 동영상</a></p>
<p>유니티의 <code>VideoPlayer</code>컴포넌트를 이용하여 유튜브와 유사한 비디오 플레이어를 구현한 스크립트</p>
<hr>
<h2 id="스크립트">스크립트</h2>
<pre><code>using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;

namespace Video
{
    public class VideoController : MonoBehaviour
    {
        [Header(&quot;Audio Source&quot;)] [SerializeField]
        private AudioSource audioSource;

        [Header(&quot;Video Player&quot;)] [SerializeField]
        private VideoPlayer videoPlayer;

        [Header(&quot;Control Parts&quot;)] [SerializeField]
        private Button startPauseButton;

        [SerializeField] private Slider speedSlider;
        [SerializeField] private Slider volumeSlider;
        [SerializeField] private Toggle loopToggle;
        [SerializeField] private Slider videoSlider;


        // private bool _isPlaying;
        // private float _currentSpeed;
        // private bool _loop;

        #region Unity Methods

        private void Start()
        {
            // add event to parts
            AddListeners();

            // Set VideoPlayer default values
            videoPlayer.SetTargetAudioSource(0, audioSource);
            volumeSlider.value = videoPlayer.GetDirectAudioVolume(0);

            SetVideoSpeed(1f);
            SetVideoVolume(1f);
            SetLoop(false);

            videoPlayer.Prepare();
        }

        private void Update()
        {
            videoSlider.SetValueWithoutNotify((float)(videoPlayer.time / videoPlayer.length));
        }

        private void OnDestroy()
        {
            RemoveListeners();
        }

        #endregion



        #region Video Controlling Methods

        public void PlayPause()
        {
            if (videoPlayer.isPlaying)
            {
                videoPlayer.Pause();
            }
            else
            {
                if (!videoPlayer.isPrepared)
                {
                    videoPlayer.Prepare();
                    return;
                }

                videoPlayer.Play();
            }
        }

        public void PlayPause(bool isPlay)
        {
            if (!isPlay)
            {
                videoPlayer.Pause();
            }
            else
            {
                if (!videoPlayer.isPrepared)
                {
                    videoPlayer.Prepare();
                    return;
                }
                videoPlayer.Play();
            }
        }

        public void SetVideoVolume(float value)
        {
            videoPlayer.SetDirectAudioVolume(0, value);
            volumeSlider.SetValueWithoutNotify(value);
        }

        public void SetVideoSpeed(float value)
        {
            videoPlayer.playbackSpeed = value;
            speedSlider.SetValueWithoutNotify(value);

        }

        public void SetLoop(bool value)
        {
            videoPlayer.isLooping = value;
            loopToggle.isOn = value;
        }

        public void SetPlayTime(float time)
        {
            var duration = videoPlayer.frameCount / (ulong)videoPlayer.frameRate;
            videoPlayer.time = time * duration;
        }

        #endregion


        #region Util Methods


        public void AddListeners()
        {
            startPauseButton.onClick.AddListener(PlayPause);
            speedSlider.onValueChanged.AddListener(SetVideoSpeed);
            volumeSlider.onValueChanged.AddListener(SetVideoVolume);

            loopToggle.onValueChanged.AddListener(SetLoop);
            videoSlider.onValueChanged.AddListener(SetPlayTime);
        }

        public void RemoveListeners()
        {
            startPauseButton.onClick.RemoveAllListeners();
            speedSlider.onValueChanged.RemoveAllListeners();
            volumeSlider.onValueChanged.RemoveAllListeners();

            loopToggle.onValueChanged.RemoveAllListeners();
            videoSlider.onValueChanged.RemoveAllListeners();
        }

        #endregion
    }
}</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 원 프로젝트 크로스플랫폼]]></title>
            <link>https://velog.io/@_hoya_/Unity-OneProject-CrossPlatform</link>
            <guid>https://velog.io/@_hoya_/Unity-OneProject-CrossPlatform</guid>
            <pubDate>Fri, 26 Jan 2024 08:52:53 GMT</pubDate>
            <description><![CDATA[<p><a href="https://youtu.be/2vYNZF4Gx2w">시연 동영상</a></p>
<p>유니티에서 하나의 프로젝트로 다양한 플랫폼에 실행할 수 있는 방법을 고안 및 정리해놓은 문서이다.</p>
<hr>
<h2 id="계기-및-명세">계기 및 명세</h2>
<h3 id="구현-계기">구현 계기</h3>
<p>둘 이상의 플랫폼을 지원하는 것을 뜻한다.</p>
<p>최근들어 모바일 기기의 성능 향상, VR/AR의 등장, 멀티플레이 게임의 발전으로 인해 크로스플랫폼을 지원하는 게임이 증가하게 되었다.
이를 뒷받침하듯 각종 게임엔진에서도 PC, 모바일, 콘솔 등 여러 플랫폼에 빌드할 수 있게 지원을 해주고 있다.</p>
<p>PC, 모바일, VR 크로스플랫폼을 지원하는 애플리케이션을 만들게 되면서 크로스플랫폼을 구현하는 방법을 많이 조사해 보았다.
대체로 하나의 플랫폼당 하나의 프로젝트를 만들어 여러 개의 프로젝트를 관리하는 방법을 사용했다.</p>
<p>위의 방법은 앱의 최적화 및 관리에 강점을 보이지만, 인원과 시간이 많이 든다.
고로 개발자가 나 하나인 현재 프로젝트에서는 하나의 개발 프로젝트로 모든 플랫폼 앱을 빌드할 수 있게 구현함을 목표로 하였다.
앱의 최적화 및 관리는 각각 어드레서블과 에셋 컴프레션, 깃의 적극적 사용으로 일부 해결하였다.</p>
<h3 id="구현-명세">구현 명세</h3>
<ul>
<li>모든 플랫폼에 대한 빌드를 하나의 프로젝트로 수행하여야 한다. 이 때 플랫폼 전환을 위한 스크립트 변경을 최소화한다.</li>
<li>라이브 서버를 이용한 실시간 멀티플레이를 적용해야 한다.</li>
<li>해당 프로젝트에서는 PC, 모바일, VR기기에 대한 크로스 플랫폼을 지원해야 한다.</li>
</ul>
<hr>
<h2 id="설정-및-사전-작업">설정 및 사전 작업</h2>
<h3 id="사전-설정">사전 설정</h3>
<p>모바일 기기와 PC기기간의 크로스 플랫폼을 수행할 때에는 두 기종간의 성능차가 구현에 큰 발목을 붙잡는다. 따라서 각 기종의 성능에 걸맞게 세팅을 수행해야 한다.</p>
<p>일반적으로 GPU 성능이 많이 낮은 모바일 기기의 특성상 텍스처 화질, 그림자 퀄리티 등 여러 그래픽적 요소에서 최적화를 수행해준다.
여러 플랫폼에 대한 각종 최적화 기법은 <a href="https://velog.io/@_hoya_/Unity-Optimization">해당 문서</a>에 모아두었다.</p>
<p>아래에는 원 빌드 멀티플레이 세팅에 사용되는 추가적인 최적화 방식을 작성하였다.</p>
<h4 id="퀄리티-세팅">퀄리티 세팅</h4>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/aa53ae01-8f7a-4ff6-ab5e-fe2f8bcea086/image.png" alt=""></p>
<h4 id="텍스처-세팅">텍스처 세팅</h4>
<p><img src="https://velog.velcdn.com/images/_hoya_/post/e4606eef-1816-43da-bc16-b0c6f08903a7/image.png" alt=""></p>
<p>대체로 모바일 기기는 화면이 작으므로 텍스처의 사이즈를 줄여도 티가 잘 나지 않는다.
따라서 텍스처의 맥스 사이즈를 줄여 GPU의 부담을 줄여준다.</p>
<h4 id="어드레서블-에셋-사용">어드레서블 에셋 사용</h4>
<p>3D 오브젝트의 경우도 위의 텍스처와 같이 기기에 따라 다르게 임포트해야 한다.
이를 메모리 낭비 없이 생성해주는 것이 <a href="https://velog.io/@_hoya_/Unity-Addressable-Asset">어드레서블 에셋</a>이다.</p>
<hr>
<h2 id="구현-및-스크립트">구현 및 스크립트</h2>
<p>원 프로젝트 멀티플랫폼을 구현하기 위해 작성한 코드들과 이에 대한 설명이다.</p>
<h3 id="플랫폼-구분">플랫폼 구분</h3>
<p>하나의 프로젝트에서 멀티플랫폼을 구현하기 위해 가장 필요한 작업은 해당 애플리케이션이 어떤 디바이스에서 작동되고 있는지를 파악하는 것이다.
유니티는 여러 인포 클래스를 통해 해당 정보들을 제공해준다.
이를 사용하기 쉽게 다음과 같이 스크립트로 작성할 수 있다.</p>
<pre><code>namespace Utils.Platform
{
    public static class CheckPlatform
    {
        public static bool IsVRPlatForm()
        {
            return XRSettings.enabled;
        }

        public static bool IsMobilePlatform()
        {
            return Application.isMobilePlatform;
        }

        public static bool IsPCPlatform()
        {
            if (Application.platform == RuntimePlatform.WindowsPlayer &amp;&amp; !IsVRPlatForm())
            {
                return true;
            }
            return false;
        }
    }
}</code></pre><h3 id="플랫폼에-따른-오브젝트-소환">플랫폼에 따른 오브젝트 소환</h3>
<p>플랫폼 구분을 하는 가장 큰 이유는 해당 플랫폼에 걸맞는 오브젝트/UI를 소환하기 위해서이다.</p>
<h4 id="플레이어">플레이어</h4>
<p>플랫폼의 변화에 따라 가장 큰 차이를 내는 것이 바로 플레이어이다. 기기에 따른 인풋 방식의 차이와 이에 따른 움직임 등을 따로 구현해야 하기 때문이다.
본인은 PC/모바일 플레이어와 VR 플레이어를 따로 구현하고, 설치한 플랫폼에 따라 적절한 플레이어 오브젝트를 소환하였다.
PC와 모바일 플레이어의 입력은 <a href="https://velog.io/@_hoya_/Unity-Input-System">인풋 시스템</a>을 이용해 처리하였다.</p>
<pre><code>namespace Manager
{
    public class PlayerSpawner : SimulationBehaviour, IPlayerJoined
    {
        public NetworkObject playerPrefab;
        public NetworkObject vrPlayerPrefab;

        /// &lt;summary&gt;
        /// 플레이어가 플레이어와 같은 게임 오브젝트에 있으면 세션에 참여할 때마다 호출되는 함수
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;player&quot;&gt;서버가 해당 플레이어를 구분하는데 사용하는 값&lt;/param&gt;
        public void PlayerJoined(PlayerRef player)
        {
            if (player == Runner.LocalPlayer)
            {
                if (CheckPlatform.IsVRPlatForm())
                {
                    Runner.Spawn(vrPlayerPrefab, new Vector3(0, 1, 0), Quaternion.identity, player);
                }
                else
                {
                    Runner.Spawn(playerPrefab, new Vector3(0, 1, 0), Quaternion.identity, player);
                }
            }
        }
    }
}</code></pre><ul>
<li>해당 프로젝트에서 PC/모바일 플레이어 캐릭터는 <code>CharacterController</code>컴포넌트를 이용해 구현하였고, VR 플레이어 캐릭터는 OpenXR의 <code>XR Origin</code>을 이용해 구현하였다.</li>
</ul>
<h4 id="오브젝트-및-ui">오브젝트 및 UI</h4>
<ul>
<li>성능에 영향을 많이 주는 오브젝트들은 플랫폼에 따라 다른 종류를 소환해야 한다. 해당 씬이 시작할 때에 플랫폼을 판단한 후 어드레서블을 이용해 오브젝트를 로드한다.</li>
<li>UI 또한 플랫폼에 따라 다르게 구현해야 하므로 각각 플랫폼에 맞는 UI를 프리팹화 한 후 게임 시작시 로드하여 생성한다.</li>
</ul>
<hr>
<h2 id="빌드">빌드</h2>
<p>기본적으로 유니티는 하나의 프로젝트에 하나의 애플리케이션을 빌드하기를 권장한다. 하지만 다음 방법을 이용해 하나의 프로젝트에서 여러 종류의 애플리케이션을 빌드할 수 있다.</p>
<ul>
<li><code>Project Settings - Player - Product Name</code>을 변경한다.</li>
<li><a href="https://www.youtube.com/watch?v=wOt8CW1XtcY">Unity Multibuild</a>를 사용한다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[유니티 자이로]]></title>
            <link>https://velog.io/@_hoya_/Unity-Gyro</link>
            <guid>https://velog.io/@_hoya_/Unity-Gyro</guid>
            <pubDate>Thu, 18 Jan 2024 07:41:01 GMT</pubDate>
            <description><![CDATA[<p><a href="https://everycommit.tistory.com/15">Gyroscope Input</a>
<a href="https://blog.naver.com/rlawndks4204/221346611798">Gyroscope Camera</a></p>
<hr>
<p>모바일 디바이스의 입력 중 하나인 자이로는 모바일 기기의 기울임, 가속 등을 가져오는 기능이다.
이를 이용해 물체의 기울임/회전 등을 수행할 수 있다.</p>
<h2 id="자이로를-이용한-ar-카메라-구현">자이로를 이용한 AR 카메라 구현</h2>
<h3 id="gyro-켜기">Gyro 켜기</h3>
<p>Unity의 Gyro는 <code>Input.Gyro</code>를 이용해 받아올 수 있다.
<code>SystemInfo.supportGyroscope</code>를 이용해 디바이스가 자이로를 사용할 수 있는 디바이스인지 확인한 후 자이로 입력을 킨다.</p>
<pre><code>private Gyroscope _gyro;

if(SystemInfo.supportsGyroscope
{
    _gyro = Input.gyro;
    _gyro.enabled = true;

}</code></pre><h3 id="gyro-입력-받아오기">Gyro 입력 받아오기</h3>
<p>Input.gyro에서는 다음과 같은 입력을 받아올 수 있다.
<img src="https://velog.velcdn.com/images/_hoya_/post/bd0f7340-5e85-4002-8de6-67eef30893ce/image.png" alt=""></p>
<p>이들 중 움직임에 관한 인풋은 다음과 같다.</p>
<ul>
<li>attitude : 장치의 기울어진 상태(rotation)에 대한 쿼터니언</li>
<li>gravity : 장치의 중력 가속도 벡터</li>
<li>rotationRate : 장치가 회전하면서 생기는 회전율</li>
<li>userAcceleration : 장치가 어딘가로 이동하면서 생기는 중력 가속도</li>
</ul>
<h3 id="입력을-카메라-회전에-적용">입력을 카메라 회전에 적용</h3>
<ul>
<li>RotationRate 사용</li>
</ul>
<pre><code>public class ManageCamera : MonoBehaviour {
    GameObject camParent;


    void Start () {
        camParent = new GameObject(&quot;CamParent&quot;);
        camParent.transform.position = this.transform.position;
        this.transform.parent = camParent.transform;
        Input.gyro.enabled = true;

    }


    void Update () {
        camParent.transform.Rotate(0, -Input.gyro.rotationRateUnbiased.y, 0);
        this.transform.Rotate(-Input.gyro.rotationRateUnbiased.x, 0, 0);

    }
}</code></pre><ul>
<li>Attitude 사용</li>
</ul>
<pre><code>public class MarklessAR : MonoBehaviour
{

    public Text text;

    private bool gyroCheck;

    private Gyroscope gyro;

    private GameObject Container;

    private Quaternion rot;


    void Start()
    {
        Container = new GameObject(&quot;Container&quot;);
        Container.transform.position = transform.position;
        transform.SetParent(Container.transform);
        gyroCheck = GyroCheck();
    }

    private bool GyroCheck()
    {
        if(SystemInfo.supportsGyroscope){
            gyro = Input.gyro;
            gyro.enabled = true;

            Container.transform.rotation = Quaternion.Euler(90f, 0f, 0f);

            rot = new Quaternion(0f, 0f, 1f, 0);

            return true;
        }
        return false;
    }

    private void Update()
    {

        if(gyroCheck)
        {
            transform.localRotation = gyro.attitude * rot;
        }
    }

}</code></pre>]]></description>
        </item>
    </channel>
</rss>