[ 섹션 0. OT ]

# 환경 설정

- 'DOTween'은 ' C#에 최적화된 빠르고 효율적이며 완전한 형태의 안전한 객체 지향 애니메이션 엔진'이다.

 ~> https://dotween.demigiant.com/download.php 에서 다운받을 수 있다.

 ~> CameraController Script에서 주석 처리된 부분은 DOTween 유료 버전 코드이다.

- 'Spine'은 2D를 사용하는 프로젝트에서 정말 많이 사용되는 Tool로 쉬운 제작, 높은 퀄리티 및 생산성을 가진다.

 ~> https://ko.esotericsoftware.com/spine-unity-download 에서 다운받을 수 있다.

 ~> https://ko.esotericsoftware.com/spine-in-depth 에서 쉽게 정리된 글을 통해 Spine의 장점을 확인할 수 있다.

 

[ 섹션 1. 코드 분석 ]

# Addressable

- 기존에는 Resources 폴더 산하에 Prefab, Sprites, Sound 등의 폴더를 생성하여 계층구조를 통해 에셋을 관리하였다.

 ~> Addressable을 통해 불필요한 에셋의 로드를 방지할 수 있다.

- [ Window ] - [ PackageManager ] 에서 Packages를 Unity Registry 로 바꾼 뒤 'Addressables' 검색 후 Install 한다.

 ~> 설치 후 [ Window ] - [ Asset Management ] - [ Groups ] - [ Create Addressables Settings ] 에서 초기 세팅을 한다.

 ~> [ New ] - [ Blank (no schema) ] 를 통해 새로운 그룹을 만들 수 있다. (그룹 단위로 묶어 관리 가능)

 ~> 변경된 사항이 있다면 [ Build ] - [ New Build ] - [ Default Build Script ] 를 해줘야 빌드시 오류가 발생하지 않는다.

 ~> 사용하고자 하는 에셋의 [ Inspector ] 창에서 [ Addressable ] 를 체크하여 추가할 수 있다.

 ~> 기본적으로 Address 값은 에셋의 경로로 설정 되지만 사용자가 임의로 수정할 수 있다. (Address 값을 통해 로드)

 ~> 에셋의 경로를 바꿔도 Path는 자동으로 수정된다.

Addressable은 비동기 호출이다.

 ~> 기존에 사용하던 Resources.Load() 는 로드가 끝나야지만 다음 코드로 넘어간다. (동기 호출)

 ~> Addressable은 당장 실행되는 것이 아니므로 콜백 함수를 사용한다. (로딩이 완료될 경우 실행하도록)

 ~> Release를 해줘야 메모리에서 사라진다.

Addressable 사용 예제
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class TestAddressable : MonoBehaviour
{
    void Start()
    {
        AsyncOperationHandle<GameObject> obj = Addressables.LoadAssetAsync<GameObject>("KeyName");
        obj.Completed += (val) =>
        {
            GameObject go = val.Result;
            Debug.Log(go.name);
            
            Addressables.Release(obj);
        };
    }
}​

 

# 스테이지와 인벤토리

- 첨부파일의 AlicePang 프로젝트를 통해 찾고자 하는 기능이 어디에, 어떤식으로 구현되어 있는지 직접 찾고 확인해보기

- 가챠 시스템에서 아이템을 뽑을때 대부분 아이템이 바로 등장하는 것이 아닌 다양한 효과가 발생한 뒤 등장한다. 

 ~> Animation도 상태를 통해 관리하는 것이 좋다.

상태를 통한 Animation 관리 예제
void OnClickTouchPanel()
    {
        Debug.Log("ClickTouchPanel");
        switch(_animationSequence)
        {
            case AnimationSequence.StartGacha:
                state.Complete -= MotionToWaitForOpenIdle;
                WaitForOpenAnimation();
                break;
            case AnimationSequence.WaitForOpenIdle:
                OpenAnimation();
                break;
            case AnimationSequence.Open:
                state.Complete -= MotionToAfterOpen;
                AfterOpenAnimation();
                break;
            case AnimationSequence.AfterOpenIdle:
                WaitForShowCardAnimation();
                break;
            case AnimationSequence.WaitForShowCardIdle:
                ShowCardAnimation();
                break;
            case AnimationSequence.ShowCard:
                ShowCardAnimation();
                break;
            case AnimationSequence.EndGacha:
                break;
        }
    }​

 

# 전투 코드 분석

- 첨부파일의 AlicePang 프로젝트를 통해 찾고자 하는 기능이 어디에, 어떤식으로 구현되어 있는지 직접 찾고 확인해보기

- Polygon Collider 2D는 2D 물리 시스템과 상호작용하는 Collider 2D 컴포넌트이다.

 ~> 모양은 라인 세그먼트의 자유형 가장자리로, 스프라이트 모양이나 다른 모양에 맞게 조정할 수 있다.

 

 

 

 

 

 

 

해당 글은 유튜버 " 고라니TV - 게임개발 채널" 님의 "유니티 포톤PUN2 서버 개발" 재생목록을 참고하여 작성하였습니다.

https://www.youtube.com/watch?v=mPCNTi3Booo&list=PL3KKSXoBRRW3YE4UMnRH762vOhSHLdnpK

 

 

[ 환경 세팅 ]

1. 우선 https://www.photonengine.com/ko-kr 에 접속하여 회원가입/로그인을 한다.

2. 오른쪽 위 [ 관리 화면으로 이동 ] - [ 새 어플리케이션 만들기 ] 를 클릭한다.

3. Photon 종류는 [ Pun ] 으로 설정하고, 어플리케이션의 이름과 설명을 작성한 뒤 [ 작성하기 ] 를 클릭한다.

4. 어플리케이션 ID를 복사하고, Unity Asset Store에서 Photon Pun 2를 다운로드 후 import 해준다.

 

5. 복사한 어플리케이션 ID를 [ Assets ] - [ Photon ] - [ PhotonUnityNetworking ] - [ Resources ] - [ Photon Server Settings ]

    - [ Server/Cloud Settings ] 의 [ App Id PUN ] 에 붙여넣는다.

 

[ Photon 기능 함수 ]

Photon 기능 함수
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun; // MonoBehaviourPunCallbacks를 상속 받기 위해 추가
using Photon.Realtime; // MonoBehaviourPunCallbacks를 상속 받기 위해 추가

public class NetworkManager : MonoBehaviourPunCallbacks // MonoBehaviour이 아닌 MonoBehaviourPunCallbacks를 상속 받는다.
{
    public Text StatusText;
    public InputField roomInput, NickNameInput;
      
    void Awake() => Screen.SetResolution(960, 540, false);
    void Update() => StatusText.text = PhotonNetwork.NetworkClientState.ToString(); // PhotonNetwork.NetworkClientState.ToString()는 현재 상태를 문자열로 반환
    
    public void Connect() => PhotonNetwork.ConnectUsingSettings(); // Photon Online Server에 접속
    public override void OnConnectedToMaster() // Call back 함수
    {
        print("서버접속완료");
        PhotonNetwork.LocalPlayer.NickName = NickNameInput.text;
    }

    public void Disconnect() => PhotonNetwork.Disconnect(); //  Photon Online Server와의 연결 끊기
    public override void OnDisconnected(DisconnectCause cause) => print("연결끊김"); // Call back 함수

    public void JoinLobby() => PhotonNetwork.JoinLobby(); // 로비에 접속
    public override void OnJoinedLobby() => print("로비접속완료"); // Call back 함수

    public void CreateRoom() => PhotonNetwork.CreateRoom(roomInput.text, new RoomOptions { MaxPlayers = 2 }); // 방 생성 (방 이름, 최대 플레이어 수, 비공개 지정 가능)
    public override void OnCreatedRoom() => print("방만들기완료"); // Call back 함수

    public void JoinRoom() => PhotonNetwork.JoinRoom(roomInput.text); // 방 참가 (방 이름으로 입장 가능)
    public override void OnJoinedRoom() => print("방참가완료"); // Call back 함수

    public void JoinOrCreateRoom() => PhotonNetwork.JoinOrCreateRoom(roomInput.text, new RoomOptions { MaxPlayers = 2 }, null); // 방 참가, 방이 없으면 생성후 참가

    public void JoinRandomRoom() => PhotonNetwork.JoinRandomRoom(); // 방 랜덤 참가

    public void LeaveRoom() => PhotonNetwork.LeaveRoom(); // 방 떠나기
 
    public override void OnCreateRoomFailed(short returnCode, string message) => print("방만들기실패"); // Call back 함수

    public override void OnJoinRoomFailed(short returnCode, string message) => print("방참가실패"); // Call back 함수

    public override void OnJoinRandomFailed(short returnCode, string message) => print("방랜덤참가실패"); // Call back 함수

    // [ Script Component ] - [ 오른쪽 마우스 ] - [ 정보 ] 를 통해 실행 가능
    [ContextMenu("정보")]
    void Info()
    {
        if (PhotonNetwork.InRoom)
        {
            print("현재 방 이름 : " + PhotonNetwork.CurrentRoom.Name);
            print("현재 방 인원수 : " + PhotonNetwork.CurrentRoom.PlayerCount);
            print("현재 방 최대인원수 : " + PhotonNetwork.CurrentRoom.MaxPlayers);

            string playerStr = "방에 있는 플레이어 목록 : ";
            for (int i = 0; i < PhotonNetwork.PlayerList.Length; i++) playerStr += PhotonNetwork.PlayerList[i].NickName + ", ";
            print(playerStr);
        }
        else
        {
            print("접속한 인원 수 : " + PhotonNetwork.CountOfPlayers);
            print("방 개수 : " + PhotonNetwork.CountOfRooms);
            print("모든 방에 있는 인원 수 : " + PhotonNetwork.CountOfPlayersInRooms);
            print("로비에 있는지? : " + PhotonNetwork.InLobby);
            print("연결됐는지? : " + PhotonNetwork.IsConnected);
        }
    }
}​

 

[ RPC란? ]

- Player의 움직임을 동기화하기 위해서는 "Photon View"와 "Photon Transform View" Script를 Component로 추가해준다.

 ~> "Photon Transform View" Script Component 의 Synchronize Options 에서 무엇을 동기화할 것인지 선택할 수 있다.

       (Position, Rotation, Scale 에 관한 동기화 선택이 가능하다.)

 ~> "Photon View" Script Component 의 Observed Components에 "Photon Transform View" Script Component 를

      드래그 앤 드롭으로 연결한다. (이는 "Photon Transform View" Script Component 를 관찰하여 동기화 한다는 것)

 ~> 동기화는 "Photon View" Script Component 의 Controlled locally가 true인 경우에만 가능하다.

      (Controlled locally는 PhotonView PV; PV.IsMine 으로 확인이 가능하다.)

- Position, Rotation, Scale 를 제외한 나머지는 어떻게 동기화 시킬까?

 ~> RPC 함수를 통해 동기화 시켜야 한다.

RPC 함수 선언 및 사용 예제
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;

public class PlayerScript : MonoBehaviourPunCallbacks
{
    public PhotonView PV;
    public SpriteRenderer SR;

    void Update()
    {
        if (PV.IsMine)
        {
            float axis = Input.GetAxisRaw("Horizontal");
            transform.Translate(new Vector3(axis * Time.deltaTime * 7, 0, 0));

            // RPC 함수 호출 (실행하고자 하는 함수 이름, 타겟, 인자)
            // RPCTarget.All은 그 즉시 호출되어 사라지지만, AllBuffered는 재접속될때 호출된다.
            if (axis != 0) PV.RPC("FlipXRPC", RpcTarget.AllBuffered, axis);
        }
    }

    // RPC 함수 선언
    [PunRPC]
    void FlipXRPC(float axis)
    {
        SR.flipX = axis == -1;
    }
}​

[ 애니매이션 동기화 ]

- Animation을 동기화하기 위해서는 "Photon View"와 "Photon Animator View" Script를 Component로 추가해준다.

 ~> "Photon Animator View" Script Component 의 Synchronize Layer Weights 와 Synchronize Parameters 선택지 중

      "Disabled" 는 "사용 안 함", "Discrete" 는 "On/Off시 호출", "Continuous" 는 "수시로 호출" 에 해당한다.

 ~> "Photon View" Script Component 의 Observed Components에 "Photon Animator View" Script Component 를

      드래그 앤 드롭으로 연결한다. (이는 "Photon Animator View" Script Component 를 관찰하여 동기화 한다는 것)

 

[ 변수 동기화 ]

- 변수를 동기화하기 위해서는 MonoBehaviourPunCallbacks 뿐만이 아닌 IPunObservable 도 상속 받는다.

 ~> IPunObservable 를 상속 받는 경우 OnPhotonSerializeView 인터페이스 구현이 필수다.

 ~> 동기화는 PhotonView를 반드시 거쳐가야 하기 때문에 위의 Script를  "Photon View" Script Component 의 Observed

      Components에 드래그 앤 드롭으로 연결한다.

변수 동기화 예제
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;

public class PlayerScript : MonoBehaviourPunCallbacks, IPunObservable // IPunObservable을 상속 받는다.
{
    // ...
    
    public Text txt;

    // ...

    [ContextMenu("더하기")]
    public void Plus() => txt.text = (int.Parse(txt.text) + 1).ToString();

    // OnPhotonSerializeView 인터페이스 구현
    public void OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting) stream.SendNext(txt.text); // Server에 보낼때
        else txt.text = (string)stream.ReceiveNext(); // Server로부터 받을때
    }
}​

 

 

 

 

 

 

[ 섹션 3. 패킷 직렬화 ]

# Serialization #1

- 직렬화란 객체를 저장 가능한 상태 또는 전송 가능한 상태로 변환하는 것을 뜻한다.

 ~> 즉, 패킷 직렬화란 메모리 상에 존재하는 데이터를 패킷에 차곡차곡 쌓은 뒤 이를 하나의 바이트 배열로 만드는 것이다.

- 역직렬화란 특정 포맷 상태의 데이터를 다시 객체로 변환하는 것을 뜻한다.

- Session은 추후에 다양하게 존재할 수 있기 때문에 Session의 이름을 정확하게 지어주는 것이 중요하다.

  (예를 들어 분산서버인 경우 각 다른 부분을 관리하는 서버의 대리자 역할을 하는 Session이 여러개 존재한다.)

- 우선 Serialization 의 흐름만 이해한 뒤 추후에 자동화 할 예정이다.

DummyClient에 ServerSession Class 생성 후 DummyClient의 Program Class 와 내용 분리
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DummyClient
{
    class Packet
    {
        public ushort size; // ushort는 2Byte
        public ushort packetId; // ushort는 2Byte
    }

    class PlayerInfoReq : Packet // Client에서 Server로 Player의 정보를 알고 싶다고 요청하는 것
    {
        public long playerId;
    }

    class PlayerInfoOk : Packet // Server에서 Client로 요청에 대한 답변을 전달하는 것
    {
        public int hp;
        public int attack;
    }

    public enum PacketID
    {
        PlayerInfoReq = 1,
        PlayerInfoOk = 2,
    }

    class ServerSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            PlayerInfoReq packet = new PlayerInfoReq() { packetId = (ushort)PacketID.PlayerInfoReq, playerId = 1001 };

            for (int i = 0; i < 5; i++)
            {
                ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

                // 아래 부분 추가 (2번의 단계를 거쳐야 하는 것을 TryWriteBytes를 통해 1번의 단계를 거치도록 수정)
                bool success = true;
                ushort count = 0; // 지금까지 몇 Byte를 Buffer에 밀어 넣었는가?
                count += 2;
                success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), packet.packetId);
                count += 2;
                success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), packet.playerId);
                count += 8;

                success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), count); // size는 모든 작업이 끝난 뒤 초기화

                // 아래 부분 삭제
                //byte[] size = BitConverter.GetBytes(packet.size); 
                //byte[] packetId = BitConverter.GetBytes(packet.packetId); 
                //byte[] playerId = BitConverter.GetBytes(packet.playerId);
                //Array.Copy(size, 0, openSegment.Array, openSegment.Offset + count, 2);
                //count += 2;
                //Array.Copy(packetId, 0, openSegment.Array, openSegment.Offset + count, 2);
                //count += 2;
                //Array.Copy(playerId, 0, openSegment.Array, openSegment.Offset + count, 8);
                //count += 8;

                ArraySegment<byte> sendBuff = SendBufferHelper.Close(count);

                if (success) // success시 Send
                    Send(sendBuff);
            }
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override int OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Server] {recvData}");
            return buffer.Count;
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes : {numOfBytes}");
        }
    }
}​

 

수정된 DummyClient의 Program Class
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ServerCore; // Servercore 라이브러리 참조

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // DNS (Domain Name System) : Domain을 IP 네트워크에서 찾아갈 수 있는 IP로 변환해 준다. 
            string host = Dns.GetHostName(); // Local Computer의 host 이름을 반환
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; // ip 주소를 배열로 반환 (예를 들어 Google과 같이 Traffic이 어마무시한 사이트는 여러개의 ip 주소를 가질 수 있기 때문)
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip 주소와 port 번호를 매개변수로 입력

            Connector connector = new Connector();
            connector.Connect(endPoint, () => { return new ServerSession(); });

            while (true)
            {
                try
                {

                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(100);
            }
        }
    }
}​

 

Server에 ClientSession Class 생성 후 Server의 Program Class 와 내용 분리
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    class Packet // 패킷 설계시 최대한 Size를 압축하여 보내는 것이 중요하다.
    {
        // Packet이 완전체로 왔는지? 잘려서 왔는지? 구분할 수 있어야 한다.
        public ushort size; // ushort는 2Byte
        public ushort packetId; // ushort는 2Byte
    }

    class PlayerInfoReq : Packet // Client에서 Server로 Player의 정보를 알고 싶다고 요청하는 것
    {
        public long playerId;
    }

    class PlayerInfoOk : Packet // Server에서 Client로 요청에 대한 답변을 전달하는 것
    {
        public int hp;
        public int attack;
    }

    public enum PacketID
    {
        PlayerInfoReq = 1,
        PlayerInfoOk = 2,
    }

    class ClientSession : PacketSession
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            Packet packet = new Packet() { size = 100, packetId = 10 };

            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
            byte[] buffer = BitConverter.GetBytes(packet.size);
            byte[] buffer2 = BitConverter.GetBytes(packet.packetId);
            // 어느 배열의? 어디서부터? 어느 배열의? 어디로? 얼마만큼?
            Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
            Array.Copy(buffer2, 0, openSegment.Array, buffer.Length, buffer2.Length);
            ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);

            Send(sendBuff);

            Thread.Sleep(5000);

            Disconnect();
        }

        // OnRecvPacket 코드 수정
        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            ushort count = 0; // 지금까지 몇 Byte를 Buffer에 밀어 넣었는가?
            ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count); // ToUInt16은 Byte 배열을 ushort로 뽑아달라는 것
            count += 2;
            ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count); // ToUInt16은 Byte 배열을 ushort로 뽑아달라는 것
            count += 2;

            switch ((PacketID)id)
            {
                case PacketID.PlayerInfoReq:
                    {
                        long playerId = BitConverter.ToInt64(buffer.Array, buffer.Offset + count); // ToUInt16은 Byte 배열을 long으로 뽑아달라는 것
                        count += 8;
                        Console.WriteLine($"PlayerInfoReq: {playerId}");
                    }
                    break;
            }

            Console.WriteLine($"RecvPacketID: {id}, Size: {size}");
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes : {numOfBytes}");
        }
    }
}​

 

수정된 Server의 Program Class
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Program
    {
        static Listener _listener = new Listener();

        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            // 손님을 입장시킨다.
            _listener.Init(endPoint, () => { return new ClientSession(); });
            Console.WriteLine("Listening...");

            while (true)
            {

            }
        }
    }
}​

 

# Serialization #2

- 자동화 하기에 앞서 인터페이스를 통한 코드 수정을 할 예정이다.

ServerSession Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DummyClient
{
    public abstract class Packet
    {
        public ushort size;
        public ushort packetId;

        // 최상위 Class인 Packet에 인터페이스 생성
        public abstract ArraySegment<byte> Write();
        public abstract void Read(ArraySegment<byte> s);
    }

    class PlayerInfoReq : Packet // Client에서 Server로 Player의 정보를 알고 싶다고 요청하는 것
    {
        public long playerId;

        // 생성자
        public PlayerInfoReq()
        {
            this.packetId = (ushort)PacketID.PlayerInfoReq;
        }

        // 인터페이스 구현
        public override ArraySegment<byte> Write()
        {
            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

            bool success = true;
            ushort count = 0; // 지금까지 몇 Byte를 Buffer에 밀어 넣었는가?
            count += 2;
            success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.packetId);
            count += 2;
            success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.playerId);
            count += 8;

            success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), count); // size는 모든 작업이 끝난 뒤 초기화

            if (success == false)
                return null;

            return SendBufferHelper.Close(count);
        }

        // 인터페이스 구현
        public override void Read(ArraySegment<byte> s)
        {
            ushort count = 0;
            //ushort size = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> 사용할 일이 없어 필요 X
            count += 2;
            //ushort id = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> Read를 실행했다는 것은 이미 패킷 분해 후 id에 대한 정보를 얻은 뒤이므로 필요 X
            count += 2;

            // 수정 부분 (Client가 악의적으로 잘못된 Packet Size를 보낸 경우를 방지하기 위함)
            this.playerId = BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));
            count += 8;
        }
    }

    // PlayerInfoOk는 추후에 구현 예정
    //class PlayerInfoOk : Packet // Server에서 Client로 요청에 대한 답변을 전달하는 것
    //{
    //    public int hp;
    //    public int attack;
    //}

    // ...

    class ServerSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            PlayerInfoReq packet = new PlayerInfoReq() { playerId = 1001 };

            for (int i = 0; i < 5; i++)
            {
                ArraySegment<byte> s = packet.Write(); // 직렬화

                if (s != null) // success시 Send
                    Send(s);
            }
        }

        // ...
    }
}​

 

ClientSession Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    public abstract class Packet
    {
        public ushort size;
        public ushort packetId;

        // 최상위 Class인 Packet에 인터페이스 생성
        public abstract ArraySegment<byte> Write();
        public abstract void Read(ArraySegment<byte> s);
    }

    class PlayerInfoReq : Packet // Client에서 Server로 Player의 정보를 알고 싶다고 요청하는 것
    {
        public long playerId;

        // 생성자
        public PlayerInfoReq()
        {
            this.packetId = (ushort)PacketID.PlayerInfoReq;
        }

        // 인터페이스 구현
        public override ArraySegment<byte> Write()
        {
            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

            bool success = true;
            ushort count = 0; // 지금까지 몇 Byte를 Buffer에 밀어 넣었는가?
            count += 2;
            success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.packetId);
            count += 2;
            success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset + count, openSegment.Count - count), this.playerId);
            count += 8;

            success &= BitConverter.TryWriteBytes(new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count), count); // size는 모든 작업이 끝난 뒤 초기화

            if (success == false)
                return null;

            return SendBufferHelper.Close(count);
        }

        // 인터페이스 구현
        public override void Read(ArraySegment<byte> s)
        {
            ushort count = 0;
            //ushort size = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> 사용할 일이 없어 필요 X
            count += 2;
            //ushort id = BitConverter.ToUInt16(s.Array, s.Offset + count); ~> Read를 실행했다는 것은 이미 패킷 분해 후 id에 대한 정보를 얻은 뒤이므로 필요 X
            count += 2;

            // 수정 부분 (Client가 악의적으로 잘못된 Packet Size를 보낸 경우를 방지하기 위함)
            this.playerId = BitConverter.ToInt64(new ReadOnlySpan<byte>(s.Array, s.Offset + count, s.Count - count));
            count += 8;
        }
    }

    // PlayerInfoOk는 추후에 구현 예정
    //class PlayerInfoOk : Packet // Server에서 Client로 요청에 대한 답변을 전달하는 것
    //{
    //    public int hp;
    //    public int attack;
    //}

    // ...

    class ClientSession : PacketSession 
    {
        // ...

        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            // 패킷을 분해하여 id 에 대한 정보를 얻은 뒤
            ushort count = 0;
            ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
            count += 2;
            ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
            count += 2;

            // 해당 id 에 맞는 코드를 실행
            switch ((PacketID)id)
            {
                case PacketID.PlayerInfoReq:
                    {
                        PlayerInfoReq p = new PlayerInfoReq();
                        p.Read(buffer); // 역직렬화
                        Console.WriteLine($"PlayerInfoReq: {p.playerId}");
                    }
                    break;
            }

            Console.WriteLine($"RecvPacketID: {id}, Size: {size}");
        }

        // ...
    }
}​

 

# UTF-8 vs UTF-16

 

- 컴퓨터가 세상에 처음 등장할 당시에는 영어와 몇가지 특수문자만을 사용하였고, 이를 저장하기 위해 1Byte로 충분했다.

 ~> ASCII 코드 (1Byte) 등장

 

- 그러나 인터넷 시대 도입 후 언어의 다양성으로 인하여 1Byte 만으로는 모든 나라의 언어를 표현할 수 없다.

 ~> UNICODE (2Byte) 등장

 

- Encoding은 컴퓨터에서 문자와 기호를 표현하기 위해 문자를 이진 데이터로 변환하는 과정이며 문자와 이진 데이터 간의

  Mapping 규칙을 정의하는 방법이다.

 ~> Variable-Width Encoding (가변 너비 인코딩) : UTF-8, UTF-16

 ~> Fixed-Length Encoding (고정 길이 인코딩) : UTF-32

 

- UTF-8

 ~> 영문 : 1Byte

 ~> 한글 : 3Byte

 

- UTF-16

 ~> BMP X : 2Byte

 ~> BMP O : 4Byte

 ~> 영문 : 2Byte

 ~> 한글 : 2Byte

 

# Serialization #3

- 데이터의 길이가 가변적인 String은 어떻게 처리해야 할까?

String 처리를 위한 ServerSession Class 와 ClientSession Class 수정
(아래 코드는 ServerSession Class지만 ClientSession Class도 동일한 코드로 수정)
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DummyClient
{
    public abstract class Packet
    {
        public ushort size;
        public ushort packetId;

        public abstract ArraySegment<byte> Write();
        public abstract void Read(ArraySegment<byte> openSegment);
    }

    class PlayerInfoReq : Packet // Client에서 Server로 Player의 정보를 알고 싶다고 요청하는 것
    {
        public long playerId;
        public string name; // 가변 길이의 멤버 변수는 어떻게 처리?

        // 생성자
        public PlayerInfoReq()
        {
            this.packetId = (ushort)PacketID.PlayerInfoReq;
        }

        public override ArraySegment<byte> Write()
        {
            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

            bool success = true;
            ushort count = 0;

            Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);

            count += sizeof(ushort);
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.packetId); // Slice는 실질적으로 Span에 변화를 주지 X
            count += sizeof(ushort);
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.playerId); // Slice는 실질적으로 Span에 변화를 주지 X
            count += sizeof(long);

            // string 처리 #1 (Buffer에 2Byte인 string len을 먼저 삽입 후 string data 삽입)
            //ushort nameLen = (ushort)Encoding.Unicode.GetByteCount(this.name); // GetByteCount()는 UTF-16 기준의 byte 배열 크기를 반환
            //success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), nameLen); // Slice는 실질적으로 Span에 변화를 주지 X
            //count += sizeof(ushort);
            //Array.Copy(Encoding.Unicode.GetBytes(this.name), 0, openSegment.Array, count, nameLen); // GetBytes()는 string을 받아 Byte 배열로 변환
            //count += nameLen;

            // string 처리 #2 (Buffer에 2Byte인 string len을 위한 공간을 남겨둔 채로 string data를 먼저 삽입 후 string len 삽입) 
            ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, name.Length, openSegment.Array, openSegment.Offset + count + sizeof(ushort)); // Buffer에 string data를 삽입함과 동시에 string len을 반환
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), nameLen);
            count += sizeof(ushort);
            count += nameLen;

            success &= BitConverter.TryWriteBytes(span, count); // size는 모든 작업이 끝난 뒤 초기화

            if (success == false)
                return null;

            return SendBufferHelper.Close(count);
        }

        public override void Read(ArraySegment<byte> openSegment)
        {
            ushort count = 0;

            ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);

            count += sizeof(ushort);
            count += sizeof(ushort);
            this.playerId = BitConverter.ToInt64(span.Slice(count, span.Length - count)); // Slice는 실질적으로 Span에 변화를 주지 X
            count += sizeof(long);

            // string 처리
            ushort nameLen = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
            count += sizeof(ushort);
            this.name = Encoding.Unicode.GetString(span.Slice(count, nameLen)); // GetString()는 Byte 배열을 받아 string으로 변환
            count += nameLen;
        }
    }

    // ...
}​

 

# Serialization #4

- 데이터의 길이가 가변적인 List는 어떻게 처리해야 할까?

List 처리를 위한 ServerSession Class 와 ClientSession Class 수정
(아래 코드는 ServerSession Class지만 ClientSession Class도 동일한 코드로 수정)
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DummyClient
{
    public abstract class Packet
    {
        public ushort size;
        public ushort packetId;

        public abstract ArraySegment<byte> Write();
        public abstract void Read(ArraySegment<byte> openSegment);
    }

    class PlayerInfoReq : Packet // Client에서 Server로 Player의 정보를 알고 싶다고 요청하는 것
    {
        public long playerId;
        public string name; 

        public struct SkillInfo
        {
            public int id;
            public short level;
            public float duration;

            public bool Write(Span<byte> span, ref ushort count) // span은 전체 Byte 배열을, count는 실시간으로 현재 어느 곳을 작업하는지
            {
                bool success = true;
                success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), id);
                count += sizeof(int);
                success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), level);
                count += sizeof(short);
                success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), duration);
                count += sizeof(float);

                return success;
            }

            public void Read(ReadOnlySpan<byte> span, ref ushort count)
            {
                id = BitConverter.ToInt32(span.Slice(count, span.Length - count));
                count += sizeof(int);
                level = BitConverter.ToInt16(span.Slice(count, span.Length - count));
                count += sizeof(short);
                duration = BitConverter.ToSingle(span.Slice(count, span.Length - count));
                count += sizeof(float);
            }
        }

        public List<SkillInfo> skills = new List<SkillInfo>(); // 가변 길이의 멤버 변수는 어떻게 처리?

        // 생성자
        public PlayerInfoReq()
        {
            this.packetId = (ushort)PacketID.PlayerInfoReq;
        }

        public override ArraySegment<byte> Write()
        {
            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

            bool success = true;
            ushort count = 0;

            Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);

            count += sizeof(ushort);
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.packetId); 
            count += sizeof(ushort);
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.playerId); 
            count += sizeof(long);

            // string 처리
            ushort nameLen = (ushort)Encoding.Unicode.GetBytes(this.name, 0, name.Length, openSegment.Array, openSegment.Offset + count + sizeof(ushort)); // Buffer에 string data를 삽입함과 동시에 string len을 반환
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), nameLen);
            count += sizeof(ushort);
            count += nameLen;

            // list 처리
            success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)skills.Count);
            count += sizeof(ushort);
            foreach (SkillInfo skill in skills)
                success &= skill.Write(span, ref count);

            success &= BitConverter.TryWriteBytes(span, count); // size는 모든 작업이 끝난 뒤 초기화

            if (success == false)
                return null;

            return SendBufferHelper.Close(count);
        }

        public override void Read(ArraySegment<byte> openSegment)
        {
            ushort count = 0;

            ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);

            count += sizeof(ushort);
            count += sizeof(ushort);
            this.playerId = BitConverter.ToInt64(span.Slice(count, span.Length - count)); 
            count += sizeof(long);

            // string 처리
            ushort nameLen = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
            count += sizeof(ushort);
            this.name = Encoding.Unicode.GetString(span.Slice(count, nameLen));
            count += nameLen;

            // list 처리
            skills.Clear();
            ushort skillLen = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
            count += sizeof(ushort);  
            for (int i = 0; i < skillLen; i++)
            {
                SkillInfo skill = new SkillInfo();
                skill.Read(span, ref count);
                skills.Add(skill);
            }
        }
    }

    // ...
}​

 

# Packet Generator #1

- [ 솔루션 ] - [ 오른쪽 마우스 ] - [ 추가 ] - [ 새 솔루션 폴더 ] 를 통해 폴더를 추가할 수 있다.

- [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 추가 ] - [ 새 항목 ] 의 [ C# 항목 ] - [ 데이터 ] 에서 XML 파일을 추가할 수 있다.

 ~> 생성된 XML 파일은 [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 파일 탐색기에서 폴더 열기 ] - [ bin ] - [ Debug ] - [ net ] 안에

      위치하도록 한다. (즉, 해당 프로젝트의 실행파일이 있는 곳에 위치하도록)

- 패킷의 정의를 어떤 방식으로 할지 결정해야 한다. (JSON, XML, 자체 정의 IDL) 

 ~> XML이 JSON에 비해 Hierarchy가 잘 보인다는 장점을 가지므로 XML을 사용할 것이다.

 ~> XML에서 정보는 시작 Tag와 끝 Tag 사이에 담겨지며, Tag는 쌍을 이룬다.

     (Tag 사이에 삽입할 정보가 없는 경우 시작 Tag 끝에 /를 추가하여 끝 Tag 생략 가능)

- List를 제외한 나머지 부분들을 자동화하기 위한 Template을 만들고자 한다.

PacketGenerator 프로젝트 생성 후 PDL XML 파일 생성
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
	<packet name="PlayerInfoReq">
		<long name="playerId"/>
		<string name="name"/>
		<list name="skill">
			<int name="id"/>
			<short name="level"/>
			<float name="duration"/>
		</list>
	</packet>
</PDL>​

 

PacketGenerator 코드 수정
using System.Xml;

namespace PacketGenerator
{
    internal class Program
    {
        static void Main(string[] args)
        {
            XmlReaderSettings settings = new XmlReaderSettings()
            {
                IgnoreComments= true, // 주석을 무시
                IgnoreWhitespace = true // 공백을 무시
            };

            // XML 파싱
            using (XmlReader reader = XmlReader.Create("PDL.xml", settings))
            {
                reader.MoveToContent(); // header를 건너뛰고 내용 부분으로 이동
                while (reader.Read()) // 한줄씩 읽어나간다.
                {
                    if (reader.Depth == 1 && reader.NodeType == XmlNodeType.Element) // Element는 시작 Tag, EndElement는 끝 Tag
                        ParsePacket(reader);

                    // Console.WriteLine(reader.Name + " " + reader["name"]); // Name은 Type을 반환, []는 Attribute를 반환
                }
            }

            // reader.Dispose(); ~> using 사용시 using 범위를 벗어날 경우 자동으로 Dispose()를 호출
        }

        public static void ParsePacket(XmlReader reader)
        {
            if (reader.NodeType == XmlNodeType.EndElement)
                return;

            if (reader.Name.ToLower() != "packet")
            {
                Console.WriteLine("Invalid packet node");
                return;
            }

            string packetName = reader["name"];
            if (string.IsNullOrEmpty(packetName) )
            {
                Console.WriteLine("Packet without name");
                return;
            }

            ParseMembers(reader);
        }

        public static void ParseMembers(XmlReader reader)
        {
            string packetName = reader["name"];

            int depth = reader.Depth + 1;
            while(reader.Read())
            {
                if (reader.Depth != depth)
                    break;

                string memberName = reader["name"];
                if (string.IsNullOrEmpty(memberName) )
                {
                    Console.WriteLine("Member without name");
                    return;
                }

                string memberType = reader.Name.ToLower();
                switch (memberType)
                {
                    case "bool":
                    case "byte":
                    case "short":
                    case "ushort":
                    case "int":
                    case "long":
                    case "float":
                    case "double":
                    case "string":
                    case "list":
                        break;
                    default:
                        break;
                }
            }
        }
    }
}​

 

PacketFormat Class 생성
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // 여러줄에 거쳐 문자열을 정의하고 싶은 경우 @"" 
        // 고정적인 부분을 제외한 변경되는 부분을 {}로 표시(일반적인 소괄호는 { { } }로 표시)

        // {0} 패킷 이름
        // {1} 멤버 변수들
        // {2} 멤버 변수 Read
        // {3} 멤버 변수 Write
        public static string packetFormat =
@"
class {0}
{{
    {1}

    public void Read(ArraySegment<byte> openSegment)
    {{
        ushort count = 0;

        ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);
        count += sizeof(ushort);
        count += sizeof(ushort);

        {2}
    }}

    public ArraySegment<byte> Write()
    {{
        ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

        bool success = true;
        ushort count = 0;

        Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);

        count += sizeof(ushort);
        success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)PacketID.{0}); 
        count += sizeof(ushort);

        {3}

        success &= BitConverter.TryWriteBytes(span, count);

        if (success == false)
            return null;

        return SendBufferHelper.Close(count);
    }}
}}
";

        // {0} 변수 형식
        // {1} 변수 이름
        public static string memberFormat =
@"public {0} {1}";

        // {0} 변수 이름
        // {1} To변수형식 (ex : ToInt16, ToInt32, ToSingle ...)
        // {2} 변수 형식
        public static string readFormat =
@"
this.{0} = BitConverter.{1}(span.Slice(count, span.Length - count));
count += sizeof({2});
";

        // {0} 변수 이름
        public static string readStringFormat =
@"
ushort {0}Len = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(span.Slice(count, {0}Len));
count += {0}Len;
";

        // {0} 변수 이름
        // {1} 변수 형식
        public static string writeFormat =
@"
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), this.{0}); 
count += sizeof({1});
";

        // {0} 변수 이름
        public static string writeStringFormat =
@"
ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, openSegment.Array, openSegment.Offset + count + sizeof(ushort)); // Buffer에 string data를 삽입함과 동시에 string len을 반환
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), {0}Len);
count += sizeof(ushort);
count += {0}Len;
";
    }
}​

 

+ 추가 검색 (https://velog.io/@mercurios0603/%ED%8C%8C%EC%8B%B1Parsing%EC%9D%B4%EB%9E%80)

 - Parsing은 컴퓨터 과학 및 프로그래밍에서 특정 형식으로 구성된 데이터를 분석하고 그 의미를 이해하는 과정을 의미

 - Parsing은 주로 텍스트 기반 데이터를 해석하거나, 프로그래밍 언어의 소스 코드를 이해하거나, 문서를 구조화하고

   내용을 추출하는 데 사용

 

# Packet Generator #2

- 지난시간 제외한 List 부분을 자동화하기 위한 Template을 만들고, Packet Generator가 잘 실행되는지 확인하고자 한다.

 ~> Packet Generator의 결과를 "GenPackets.cs" 파일에 저장하였다.

 ~> "GenPackets.cs" 파일은 [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 파일 탐색기에서 폴더 열기 ] - [ bin ] - [ Debug ] - [ net ]

      에서 확인할 수 있다.

PacketFormat Class 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // ...

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        // {2} 멤버 변수들
        // {3} 멤버 변수 Read
        // {4} 멤버 변수 Write
        public static string memberListFormat =
@"
public struct {0}
{{
    {2}

    public void Read(ReadOnlySpan<byte> span, ref ushort count)
    {{
        {3}
    }}

    public bool Write(Span<byte> span, ref ushort count)
    {{
        bool success = true;
        {4}
        return success;
    }}
}}

public List<{0}> {1}s = new List<{0}>();
";

        // ...

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string readListFormat =
@"
this.{1}s.Clear();
ushort {1}Len = BitConverter.ToUInt16(span.Slice(count, span.Length - count));
count += sizeof(ushort);  
for (int i = 0; i < {1}Len; i++)
{{
    {0} {1} = new {0}();
    {1}.Read(span, ref count);
    {1}s.Add({1});
}}
";

        // ...

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string writeListFormat =
@"
success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)this.{1}s.Count);
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
    success &= {1}.Write(span, ref count);
";
    }
}

 

PacketGenerator 코드 수정
using System.Xml;

namespace PacketGenerator
{
    internal class Program
    {
        static string genPackets; // 실시간으로 만들어지는 패킷

        static void Main(string[] args)
        {
            XmlReaderSettings settings = new XmlReaderSettings()
            {
                IgnoreComments= true, // 주석을 무시
                IgnoreWhitespace = true // 공백을 무시
            };

            // XML 파싱
            using (XmlReader reader = XmlReader.Create("PDL.xml", settings))
            {
                reader.MoveToContent(); // header를 건너뛰고 내용 부분으로 이동
                while (reader.Read()) // 한줄씩 읽어나간다.
                {
                    if (reader.Depth == 1 && reader.NodeType == XmlNodeType.Element) // Element는 시작 부분, EndElement는 끝 부분
                        ParsePacket(reader);

                    // Console.WriteLine(reader.Name + " " + reader["name"]); // Name은 Type을 반환, []는 Attribute를 반환
                }
            }

            File.WriteAllText("GenPackets.cs", genPackets); // genPackets의 내용을 통해 GenPackets.cs 파일 생성
        }

        public static void ParsePacket(XmlReader reader)
        {
            if (reader.NodeType == XmlNodeType.EndElement)
                return;

            if (reader.Name.ToLower() != "packet")
            {
                Console.WriteLine("Invalid packet node");
                return;
            }

            string packetName = reader["name"];
            if (string.IsNullOrEmpty(packetName) )
            {
                Console.WriteLine("Packet without name");
                return;
            }

            Tuple<string, string, string> tuple = ParseMembers(reader);
            genPackets += string.Format(PacketFormat.packetFormat,
                packetName, tuple.Item1, tuple.Item2, tuple.Item3);
        }

        // 멤버 변수들, 멤버 변수 Read, 멤버 변수 Write 에 관한 코드를 알맞게 제작한 뒤 이를 string으로 반환
        public static Tuple<string, string, string> ParseMembers(XmlReader reader)
        {
            string packetName = reader["name"];
            string memberCode = "";
            string readCode = "";
            string writeCode = "";

            int depth = reader.Depth + 1;
            while(reader.Read())
            {
                if (reader.Depth != depth)
                    break;

                string memberName = reader["name"];
                if (string.IsNullOrEmpty(memberName) )
                {
                    Console.WriteLine("Member without name");
                    return null;
                }

                if (string.IsNullOrEmpty(memberCode) == false) // 이미 내용이 존재하는 경우
                    memberCode += Environment.NewLine; // Enter를 치는 것과 같은 동작
                if(string.IsNullOrEmpty(readCode) == false) // 이미 내용이 존재하는 경우
                    readCode += Environment.NewLine; // Enter를 치는 것과 같은 동작
                if(string.IsNullOrEmpty(writeCode) == false) // 이미 내용이 존재하는 경우
                    writeCode += Environment.NewLine; // Enter를 치는 것과 같은 동작

                string memberType = reader.Name.ToLower();
                switch (memberType)
                {
                    case "bool":
                    case "short":
                    case "ushort":
                    case "int":
                    case "long":
                    case "float":
                    case "double":
                        memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
                        readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
                        writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
                        break;
                    case "string":
                        memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
                        readCode += string.Format(PacketFormat.readStringFormat, memberName);
                        writeCode += string.Format(PacketFormat.writeStringFormat, memberName);
                        break;
                    case "list":
                        Tuple<string, string, string> tuple = ParseList(reader);
                        memberCode += tuple.Item1;
                        readCode += tuple.Item2;
                        writeCode += tuple.Item3;
                        break;
                    default:
                        break;
                }
            }

            // 가독성을 위해 Text를 정렬
            memberCode = memberCode.Replace("\n", "\n\t"); // Enter가 입력된 곳은 Enter 입력 후 Tab 까지 입력되도록 수정
            readCode = readCode.Replace("\n", "\n\t\t");  // Enter가 입력된 곳은 Enter 입력 후 Tab Tab 까지 입력되도록 수정
            writeCode = writeCode.Replace("\n", "\n\t\t");  // Enter가 입력된 곳은 Enter 입력 후 Tab Tab 까지 입력되도록 수정
            return new Tuple<string, string, string>(memberCode, readCode, writeCode);
        }

        public static Tuple<string, string, string> ParseList(XmlReader reader)
        {
            string listName = reader["name"];
            if (string.IsNullOrEmpty(listName))
            {
                Console.WriteLine("List without name");
                return null;
            }

            // memberListFormat의 {2}, {3}, {4}는 순서대로 멤버 변수들, 멤버 변수 Read, 멤버 변수 Write 이므로 ParseMembers() 함수 사용 
            Tuple<string, string, string> tuple = ParseMembers(reader);

            string memberCode = string.Format(PacketFormat.memberListFormat,
                FirstCharToUpper(listName), FirstCharToLower(listName),
                tuple.Item1, tuple.Item2, tuple.Item3);

            string readCode = string.Format(PacketFormat.readListFormat,
                FirstCharToUpper(listName), FirstCharToLower(listName));

            string writeCode = string.Format(PacketFormat.writeListFormat,
                FirstCharToUpper(listName), FirstCharToLower(listName));

            return new Tuple <string, string, string> (memberCode, readCode, writeCode);
        }

        public static string FirstCharToUpper(string input)
        {
            if (string.IsNullOrEmpty(input))
                return "";
            return input[0].ToString().ToUpper() + input.Substring(1);
        }

        public static string FirstCharToLower(string input)
        {
            if (string.IsNullOrEmpty(input))
                return "";
            return input[0].ToString().ToLower() + input.Substring(1);
        }

        public static string ToMemberType(string memberType)
        {
            switch (memberType)
            {
                case "bool":
                    return "ToBoolean";
                case "short":
                    return "ToInt16";
                case "ushort":
                    return "ToUInt16";
                case "int":
                    return "ToInt32";
                case "long":
                    return "ToInt64";
                case "float":
                    return "ToSingle";
                case "double":
                    return "ToDouble";
                default:
                    return "";
            } 
        }
    }
}​

 

# Packet Generator #3

- using 과 enum 및 byte 부분을 자동화하기 위한 Template을 만들고, 패킷이 여러개 존재해도 자동화가 잘 되는지

  확인하고자 한다.

PDL XML 파일 수정 (패킷 및 sbyte 멤버 변수 추가)
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
	<packet name="PlayerInfoReq">
		<sbyte name="testByte"/>
		<long name="playerId"/>
		<string name="name"/>
		<list name="skill">
			<int name="id"/>
			<short name="level"/>
			<float name="duration"/>
		</list>
	</packet>
	<packet name="Test">
		<int name="testInt"/>
	</packet>
</PDL>​

 

PacketFormat Class 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
        public static string fileFormat =
@"
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;

public enum PacketID
{{
    {0}
}}

{1}
";
        // {0} 패킷 이름
        // {1} 패킷 번호
        public static string packetEnumFormat =
@"{0} = {1},";

        // ...

        // {0} 변수 이름
        // {1} 변수 형식
        public static string readByteFormat =
@"
this.{0} = ({1})openSegment.Array[openSegment.Offset + count];
count += sizeof({1});
";

        // ...

        // {0} 변수 이름
        // {1} 변수 형식
        public static string writeByteFormat =
@"
openSegment.Array[openSegment.Offset + count] = (byte)this.{0};
count += sizeof({1});
";

        // ...
    }
}​

 

PacketGenerator 코드 수정
using System.Xml;

namespace PacketGenerator
{
    internal class Program
    {
        // ...

        static void Main(string[] args)
        {
            // ...
            
            static ushort packetId; // 몇개의 패킷을 처리하였는지
            static string packetEnums; // Parsing 처리된 패킷의 이름과 번호

            // fileFormat을 통해 using과 enum 추가
            string fileText = string.Format(PacketFormat.fileFormat, packetEnums, genPackets);
            File.WriteAllText("GenPackets.cs", fileText);
        }

        public static void ParsePacket(XmlReader reader)
        {
            // ...

            Tuple<string, string, string> tuple = ParseMembers(reader);

            genPackets += string.Format(PacketFormat.packetFormat,
                packetName, tuple.Item1, tuple.Item2, tuple.Item3);

            // 패킷 1개를 Parsing 할때마다 packetEnums에 Parsing 처리된 패킷의 이름과 번호를 저장
            packetEnums += string.Format(PacketFormat.packetEnumFormat, packetName, ++packetId) + Environment.NewLine + "\t";
        }

        // 멤버 변수들, 멤버 변수 Read, 멤버 변수 Write 에 관한 코드를 알맞게 제작한 뒤 이를 string으로 반환
        public static Tuple<string, string, string> ParseMembers(XmlReader reader)
        {
            string packetName = reader["name"];
            string memberCode = "";
            string readCode = "";
            string writeCode = "";

            int depth = reader.Depth + 1;
            while(reader.Read())
            {
                // ...

                if (string.IsNullOrEmpty(memberCode) == false) // 이미 내용이 존재하는 경우
                    memberCode += Environment.NewLine; // Enter를 치는 것과 같은 동작
                if(string.IsNullOrEmpty(readCode) == false) // 이미 내용이 존재하는 경우
                    readCode += Environment.NewLine; // Enter를 치는 것과 같은 동작
                if(string.IsNullOrEmpty(writeCode) == false) // 이미 내용이 존재하는 경우
                    writeCode += Environment.NewLine; // Enter를 치는 것과 같은 동작

                string memberType = reader.Name.ToLower();
                switch (memberType)
                {
                    case "byte":
                    case "sbyte":
                        memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
                        readCode += string.Format(PacketFormat.readByteFormat, memberName, memberType);
                        writeCode += string.Format(PacketFormat.writeByteFormat, memberName, memberType);
                        break;
                    case "bool":
                    case "short":
                    case "ushort":
                    case "int":
                    case "long":
                    case "float":
                    case "double":
                        memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
                        readCode += string.Format(PacketFormat.readFormat, memberName, ToMemberType(memberType), memberType);
                        writeCode += string.Format(PacketFormat.writeFormat, memberName, memberType);
                        break;
                    case "string":
                        memberCode += string.Format(PacketFormat.memberFormat, memberType, memberName);
                        readCode += string.Format(PacketFormat.readStringFormat, memberName);
                        writeCode += string.Format(PacketFormat.writeStringFormat, memberName);
                        break;
                    case "list":
                        Tuple<string, string, string> tuple = ParseList(reader);
                        memberCode += tuple.Item1;
                        readCode += tuple.Item2;
                        writeCode += tuple.Item3;
                        break;
                    default:
                        break;
                }
            }

            // ...
        }

        // ...
    }
}​

 

# Packet Generator #4

- Packet Generator의 결과가 저장된 "GenPackets.cs" 파일의 내용을 수동으로 ServerSession 과 ClientSession class에

 추가하였는데 해당 부분을 자동화하기 위한 Template을 만들고자 한다.

- 출력 경로는 [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 속성 ] - [ 빌드 ] - [ 일반 ] 의 [ 출력 ] 에서 설정 가능하다.

 ~> 그 후 [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 파일 탐색기에서 폴더 열기 ] 에서

      "프로젝트이름.csproj" 파일을 메모장으로 연 뒤 <PropertyGroup></PropertyGroup> 사이에                         

      <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> 를 추가하고,

      <BaseOutputPath>출력 경로</BaseOutputPath> 를 <OutputPath>출력 경로</OutputPath> 로 수정한다.

 ~> 모든 설정이 끝난 뒤 [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 빌드 ] 시 설정한 출력 경로에 실행파일이 생성된다.

- 배치 파일은 명령 인터프리터에 의해 실행되게끔 공안된 명령어들이 나열된 텍스트 파일이다.

 ~> .bat 또는 .cmd 형식의 확장자 파일을 직접 실행하거나 명령 프롬프트에서 배치 파일의 이름으로 실행할 수 있다.

 ~> 배치파일을 통해 [ 프로젝트 ] - [ 오른쪽 마우스 ] - [ 파일 탐색기에서 폴더 열기 ] - [ bin ] 내의 프로젝트 실행파일을

      대신 실행한 뒤 실행 파일의 결과에 저장된 내용을 자동으로 복사할 예정이다. (출력 경로를 /bin 으로 설정)

 ~> START는 응용 프로그램 실행 명령어로 실행하고자 하는 파일의 경로와 인자를 넘겨준다.

 ~> XCOPY는 파일 복사 명령어로 복사 대상과 복사 위치를 넘겨준다.

     (/Y 옵션은 같은 파일이 있는 경우 무조건 덮어쓴다는 것이다.)

GenPackets.bat 배치 파일 생성 (배치 파일은 [ 솔루션 ] - [ Common ] - [ Packet ] 내에 존재)

(배치 파일 실행시 PDL.xml을 인자로 넘긴 Packet Generator가 실행되어 DummyClient/Packet 과 Server/Packet 산하의 GenPackets에 Packet Generator의 결과가 저장된 "GenPackets.cs" 파일의 내용이 자동으로 복사된다.)

START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"​

 

PacketGenerator 코드 수정
using System.Xml;

namespace PacketGenerator
{
    internal class Program
    {
        // ...

        static void Main(string[] args)
        {
            string pdlPath = "../PDL.xml"; // bin 폴더가 아닌 PacketGenerator 폴더에 있는 PDL.xml 찾기 위한 것 (실행 파일이 위치한 곳 기준으로 이동)

            // ...

            if (args.Length >= 1) // 프로그램 실행시 인자로 무언가를 넘겨준 경우
                pdlPath = args[0]; // pdlPath를 전달받은 인자로 초기화

            using (XmlReader reader = XmlReader.Create(pdlPath, settings))
            {
                // ...
            }

            // ...
        }

        // ...
    }
}​

 

# Packet Generator #5

- 게임 규모가 커질수록 패킷의 종류는 상당히 많아진다.

 ~> 즉, ClientSession Class의 OnRecvPacket() 함수 안의 switch-case문 길이가 상당히 길어질 수 있다.

- switch-case문 및 OnRecvPacket() 함수 자동화를 위해 모든 패킷들이 base interface를 상속받도록 한다.

 ~> 모든 패킷에 대해 공통 인수로 넘길 수 있기 때문에 편리하다는 장점을 가진다.

PacketFormat Class 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
        public static string fileFormat =
@"
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;

public enum PacketID
{{
    {0}
}}

interface IPacket
{{
	ushort Protocol {{ get; }}
	void Read(ArraySegment<byte> openSegment);
	ArraySegment<byte> Write();
}}

{1}
";
        // ...

        // {0} 패킷 이름
        // {1} 멤버 변수들
        // {2} 멤버 변수 Read
        // {3} 멤버 변수 Write
        public static string packetFormat =
@"
public class {0} : IPacket
{{
    {1}

    public ushort Protocol {{ get {{ return (ushort)PacketID.{0}; }} }}

    public void Read(ArraySegment<byte> openSegment)
    {{
        ushort count = 0;

        ReadOnlySpan<byte> span = new ReadOnlySpan<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);
        count += sizeof(ushort);
        count += sizeof(ushort);

        {2}
    }}

    public ArraySegment<byte> Write()
    {{
        ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);

        bool success = true;
        ushort count = 0;

        Span<byte> span = new Span<byte>(openSegment.Array, openSegment.Offset, openSegment.Count);

        count += sizeof(ushort);
        success &= BitConverter.TryWriteBytes(span.Slice(count, span.Length - count), (ushort)PacketID.{0}); 
        count += sizeof(ushort);

        {3}

        success &= BitConverter.TryWriteBytes(span, count);

        if (success == false)
            return null;

        return SendBufferHelper.Close(count);
    }}
}}
";

        // ...
    }
}​

 

DummyClient/Packet 과 Server/Packet 산하에 PacketHandler Class 생성
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace Server
{
    class PacketHandler 
    {
        // 해당 패킷이 전부 조립된 경우 무엇을 할까?
        // PacketHandler는 자동화 없이 수동으로 추가
        public static void PlayerInfoReqHandler(PacketSession session, IPacket packet)
        {
            PlayerInfoReq p = packet as PlayerInfoReq;

            Console.WriteLine($"PlayerInfoReq: {p.playerId} {p.name}");

            foreach (PlayerInfoReq.Skill skill in p.skills)
            {
                Console.WriteLine($"Skill({skill.id})({skill.level})({skill.duration})");
            }
        }
    }
}​

 

DummyClient/Packet 과 Server/Packet 산하에 PacketManager Class 생성
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace Server
{
    class PacketManager
    {
        // PacketManager는 Singleton 패턴 사용
        #region Singleton
        static PacketManager _instance;
        public static PacketManager Instance
        {
            get { 
                if (_instance == null)
                    _instance = new PacketManager();
                return _instance; 
            }
        }
        #endregion

        // 구분하기 위한 Protocol ID, 어떤 작업을 수행할지
        Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
        // 구분하기 위한 Protocol ID, 어떤 Handler를 호출할지
        Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();

        public void Register() // 추후 자동화할 예정
        {
            _onRecv.Add((ushort)PacketID.PlayerInfoReq, MakePacket<PlayerInfoReq>);
            _handler.Add((ushort)PacketID.PlayerInfoReq, PacketHandler.PlayerInfoReqHandler);
        }

        public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer)
        {
            // 패킷을 분해하여 id 에 대한 정보를 얻은 뒤
            ushort count = 0;
            ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
            count += 2;
            ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
            count += 2;

            // 이젠 switch-case문이 아닌 Dictionary에서 찾아 Invoke()
            Action<PacketSession, ArraySegment<byte>> action = null;
            if (_onRecv.TryGetValue(id, out action))
                action.Invoke(session, buffer);
        }

        void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
        {
            T packet = new T();
            packet.Read(buffer); // 역직렬화

            // Dictionary에서 찾아 Invoke()
            Action<PacketSession, IPacket> action = null;
            if (_handler.TryGetValue(packet.Protocol, out action))
                action.Invoke(session, packet);
        }
    }
}​

 

ClientSession Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    class ClientSession : PacketSession 
    {
        // ...

        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            PacketManager.Instance.OnRecvPacket(this, buffer);
        }

        // ...
    }
}​

 

Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Program
    {
        // ...

        static void Main(string[] args)
        {
            // MultiThread가 개입하지 않는 부분에서 실행
            PacketManager.Instance.Register();

            // ...
        }
    }
}​

 

# Packet Generator #6

- PacketManager 를 자동화하기 위한 Template을 만들고자 한다.

 ~> 양방향 패킷은 거의 존재하지 않는다. (대부분 Client에서 Server 또는 Server에서 Client)

 ~> 그러나 분산 Server인 경우 Client와 Server만 소통하는 것이 아닌 Server 끼리도 소통한다.

 ~> 패킷 이름에 규칙을 설정하고 이를 통해 파일을 분리한다.

     (패킷 이름 앞에 C_ 가 붙은 것은 Client에서 Server로, S_ 가 붙은 것은 Server에서 Client로)

     (PacketManager 를 자동화할때 Server 쪽에 추가되는 PacketManager의 Register에는 C_가 붙은 것들만,
      DummyClient 쪽에 추가되는 PacketManager의 Register에는 S_가 붙은 것들만 등록한다.)

     (즉, Register에 온갖 패킷을 등록하는 것이 아닌 필요한 패킷들만 등록한다.)

PDL XML 파일 수정
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
	<packet name="C_PlayerInfoReq">
		<sbyte name="testByte"/>
		<long name="playerId"/>
		<string name="name"/>
		<list name="skill">
			<int name="id"/>
			<short name="level"/>
			<float name="duration"/>
		</list>
	</packet>
	<packet name="Test">
		<int name="testInt"/>
	</packet>
</PDL>​

 

PacketFormat Class 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 등록
        public static string managerFormat =
@"
using ServerCore;
using System;
using System.Collections.Generic;

class PacketManager
{{
    #region Singleton
    static PacketManager _instance;
    public static PacketManager Instance
    {{
        get 
        {{
            if (_instance == null)
                _instance = new PacketManager();
            return _instance; 
        }}
    }}
    #endregion

    Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
    Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();

    public void Register() 
    {{
{0}
    }}

    public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer)
    {{
        ushort count = 0;
        ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
        count += 2;
        ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
        count += 2;

        Action<PacketSession, ArraySegment<byte>> action = null;
        if (_onRecv.TryGetValue(id, out action))
            action.Invoke(session, buffer);
    }}

    void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
    {{
        T packet = new T();
        packet.Read(buffer);

        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(packet.Protocol, out action))
            action.Invoke(session, packet);
    }}
}}
";

        // {0} 패킷 이름
        public static string managerRegisterFormat =
@"
        _onRecv.Add((ushort)PacketID.{0}, MakePacket<{0}>);
        _handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler);
";

        // ...
    }
}​

 

PacketGenerator 코드 수정
using System.Xml;

namespace PacketGenerator
{
    internal class Program
    {
        // ...

        static string clientRegister;
        static string serverRegister;

        static void Main(string[] args)
        {
            // ...
            
            string clientManagerText = string.Format(PacketFormat.managerFormat, clientRegister);
            File.WriteAllText("ClientPacketManager.cs", clientManagerText);
            string serverManagerText = string.Format(PacketFormat.managerFormat, serverRegister);
            File.WriteAllText("ServerPacketManager.cs", serverManagerText);
        }

        public static void ParsePacket(XmlReader reader)
        {
            // ...
            
            if (packetName.StartsWith("S_") || packetName.StartsWith("s_"));
                clientRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
            else
                serverRegister += string.Format(PacketFormat.managerRegisterFormat, packetName) + Environment.NewLine;
        }

        // ...
    }
}​

 

GenPackets.bat 배치 파일 수정
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y ClientPacketManager.cs "../../DummyClient/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"

 

[ 섹션 4. Job Queue ]

# 채팅 테스트 #1

- Server 구현시 대부분 채팅 프로그램을 통해 테스트가 이루어진다.

- 우선 ServerCore에 예외 처리를 하지 않은 부분이 존재하기 때문에 이를 수정할 예정이다.

 ~> Disconnect() 중복 호출은 예방하였으나, Disconnect() 호출시 Send와 Receive는 끊김에 대한 예방이 없다.

     (동시다발적으로 누군가는 Disconnect를 통해 socket을 shutdown하고, 누군가는 Send나 Receive를 호출시 문제 발생)

- 또한 채팅 테스트를 위해 Server 입장에서 코드 추가 및 수정을 할 예정이다.

PDL XML 파일 수정
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
	<packet name="C_Chat">
		<string name="chat"/>
	</packet>
	<packet name="S_Chat">
		<int name="playerId"/>
		<string name ="chat"/>
	</packet>
</PDL>​

 

예외 처리를 위해 Session Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    // ...

    public abstract class Session
    {
        // ...

        void Clear() // _sendQueue 와 _pendingList 를 초기화하기 위한 함수 추가
        {
            lock (_lock)
            {
                _sendQueue.Clear();
                _pendingList.Clear();
            }
        }

        // ...

        public void Disconnect()
        {
            // ...

            Clear(); // _sendQueue 와 _pendingList 를 초기화
        }

        void RegisterSend()
        {
            if (_disconnected == 1) // 최소한의 예방책
                return;

            while (_sendQueue.Count > 0)
            {
                ArraySegment<byte> buff = _sendQueue.Dequeue();
                _pendingList.Add(buff);
            }
            _sendArgs.BufferList = _pendingList;

            // socket을 다루는 부분을 try-catch문으로 감싸준다. (MultiThread 환경을 위한 예방책)
            // ~> 누군가는 위의 if문을 통과하여 아래 부분을 마저 실행하려고 하는 도중에 다른 Thread에서 socket을 disconnect시 문제가 발생하기 때문
            try
            {
                bool pending = _socket.SendAsync(_sendArgs);
                if (pending == false)
                    OnSendCompleted(null, _sendArgs);
            }
            catch (Exception e)
            {
                Console.WriteLine($"RegisterSend Failed {e}");
            }
        }

       // ...

        void RegisterRecv()
        {
            if (_disconnected == 1) // 최소한의 예방책
                return;

            _recvBuffer.Clean();
            ArraySegment<byte> segment = _recvBuffer.WriteSegment;
            _recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);

            // socket을 다루는 부분을 try-catch문으로 감싸준다. (MultiThread 환경을 위한 예방책)
            // ~> 누군가는 위의 if문을 통과하여 아래 부분을 마저 실행하려고 하는 도중에 다른 Thread에서 socket을 disconnect시 문제가 발생하기 때문
            try
            {
                bool pending = _socket.ReceiveAsync(_recvArgs);
                if (pending == false)
                    OnRecvCompleted(null, _recvArgs);
            }
            catch(Exception e)
            {
                Console.WriteLine($"RegisterRecv Failed {e}");
            }
        }

        // ...
    }
}​

 

Server에 GameRoom Class 생성
using System;
using System.Collections.Generic;
using System.Text;

namespace Server
{
    class GameRoom
    {
        List<ClientSession> _sessions = new List<ClientSession>(); // GameRoom에 존재하는 session들
        object _lock = new object(); // List나 Dictionary 등 대부분의 자료 구조들은 MultiThread 환경에서 잘 돌아간다는 보장이 없기 때문에 lock 생성

        public void Broadcast(ClientSession session, string chat) // 현재 session이 접속중인 방에 존재하는 모두에게 chat을 뿌린다.
        {
            S_Chat packet = new S_Chat();
            packet.playerId = session.SessionId;
            packet.chat = chat;
            ArraySegment<byte> segment = packet.Write();

            lock(_lock)
            {
                foreach (ClientSession s in _sessions)
                    s.Send(segment);
            }
        }

        public void Enter(ClientSession session) // 방 입장
        {
            lock (_lock)
            {
                _sessions.Add(session);
                session.Room = this;
            }   
        }

        public void Leave(ClientSession session) // 방 퇴장
        {
            lock (_lock)
            {
                _sessions.Remove(session);
            }
        }
    }
}​

 

Server에 SessionManager Class 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Server
{
    class SessionManager // SessionManager는 Engine 쪽에서 관리해도 되고, Content 쪽에서 관리해도 된다. (선택의 차이)
    {
        // SessionManager는 Singleton 패턴 사용
        static SessionManager _session = new SessionManager();
        public static SessionManager Instance { get { return _session; } }

        int _sessionId = 0; // session을 구분하기 위한 Id
        Dictionary<int, ClientSession> _sessions = new Dictionary<int, ClientSession>(); // 현재 존재하는 session들
        object _lock = new object(); // List나 Dictionary 등 대부분의 자료 구조들은 MultiThread 환경에서 잘 돌아간다는 보장이 없기 때문에 lock 생성

        public ClientSession Generate() // session 생성
        {
            lock (_lock)
            {
                int sessionId = ++_sessionId;

                ClientSession session = new ClientSession();
                session.SessionId = sessionId;
                _sessions.Add(sessionId, session);

                Console.WriteLine($"Connected : {sessionId}");

                return session;
            }
        }

        public ClientSession Find(int id) // sessionId를 통해 session을 찾는 함수
        {
            lock (_lock)
            {
                ClientSession session = null;
                _sessions.TryGetValue(id, out session);
                return session;
            }
        }

        public void Remove(ClientSession session) // session 삭제
        {
            lock (_lock)
            {
                _sessions.Remove(session.SessionId);
            }
        }
    }
}​

 

Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Program
    {
        static Listener _listener = new Listener();
        public static GameRoom Room = new GameRoom(); // GameRoom 생성 (딴 곳에서도 접근이 가능하도록 public으로 생성)

        static void Main(string[] args)
        {
            PacketManager.Instance.Register();

            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            // 손님을 입장시킨다.
            _listener.Init(endPoint, () => { return SessionManager.Instance.Generate(); }); // new를 통해 Session을 생성하는 것이 아닌 SessionManager를 통해 생성하도록 수정
            Console.WriteLine("Listening...");

            while (true)
            {

            }
        }
    }
}​

 

ClientSession 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    class ClientSession : PacketSession 
    {
        public int SessionId { get; set; } // Session 구분을 위해
        public GameRoom Room { get; set; } // 현재 어떤 방에 위치하는지 알기 위해

        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            Program.Room.Enter(this); // Client가 접속시 방에 입장시킨다. (Program 산하에 static으로 Room을 생성하였기 때문에 다음과 같이 호출)

            Thread.Sleep(5000);
            Disconnect();
        }

        // ...

        public override void OnDisconnected(EndPoint endPoint)
        {
            SessionManager.Instance.Remove(this); // 내 자신을 (즉, session을) sessionManager를 통해 삭제 요청
            if (Room != null) // 
            {
                Room.Leave(this);
                Room = null;
            }

            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        // ...
    }
}​

 

Server의 PacketHandler 수정
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

class PacketHandler 
{
    // 해당 패킷이 전부 조립된 경우 무엇을 할까?
    // PacketHandler는 자동화 없이 수동으로 추가
    public static void C_ChatHandler(PacketSession session, IPacket packet)
    {
        C_Chat chatPacket = packet as C_Chat;
        ClientSession clientSession = session as ClientSession;

        if (clientSession.Room == null)
            return;

        clientSession.Room.Broadcast(clientSession, chatPacket.chat); // 현재 clientSession이 접속중인 방에 존재하는 모두에게 채팅 메시지를 뿌린다.
    }
}​

 

# 채팅 테스트 #2

- 채팅 테스트를 위해 Client 입장에서 코드 추가 및 수정을 할 예정이다.

 ~>  현재 1명의 유저만 접속하고 있는 상황이므로, 이를 다수의 유저들이 접속하는 상황으로 가정하고 변경할 예정이다.

- 기존 Server의 Register 부분을 생성자를 통해 자동으로 생성하도록 변경할 예정이다.

ServerSession Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DummyClient
{
    class ServerSession : PacketSession // Session이 아닌 PacketSession 을 상속 받도록 수정
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override void OnRecvPacket(ArraySegment<byte> buffer) // PacketSession 을 상속 받으므로 OnRecv가 아닌 OnRecvPacket으로 수정 (반환값도 int가 아닌 void로)
        {
            PacketManager.Instance.OnRecvPacket(this, buffer);
        }

        public override void OnSend(int numOfBytes)
        {
            // Console.WriteLine($"Transferred bytes : {numOfBytes}"); ~> session이 많아지면 OnSend() 가 자주 호출되므로 일단은 출력되지 않도록 주석 처리
        }
    }
}

 

ServerCore의 Connector 수정
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ServerCore
{
    public class Connector
    {
        Func<Session> _sessionFactory;

        public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory, int count = 1) // 다수의 Client 환경에서 Test 하고 싶을 수도 있기 때문에
        {
            for (int i = 0; i < count; i++) // 입력받은 매개변수 count 만큼 아래의 과정을 반복하도록 수정
            {
                // 휴대폰 설정
                Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                _sessionFactory = sessionFactory;

                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.Completed += OnConnectCompleted;
                args.RemoteEndPoint = endPoint;
                args.UserToken = socket;

                RegisterConnect(args);
            } 
        }

        // ...
    }
}​

 

DummyClient에 SessionManager Class 생성
using System;
using System.Collections.Generic;
using System.Text;

namespace DummyClient
{
    class SessionManager
    {
        // SessionManager는 Singleton 패턴 사용
        static SessionManager _session = new SessionManager();
        public static SessionManager Instance { get { return _session; } }

        List<ServerSession> _sessions = new List<ServerSession>(); // 현재 존재하는 session들
        object _lock = new object(); // List나 Dictionary 등 대부분의 자료 구조들은 MultiThread 환경에서 잘 돌아간다는 보장이 없기 때문에 lock 생성

        public void SendForEach() // Server쪽으로 채팅 패킷을 전송
        {
            foreach (ServerSession session in _sessions)
            {
                C_Chat chatPacket = new C_Chat();
                chatPacket.chat = $"Hello Server!";
                ArraySegment<byte> segment = chatPacket.Write();

                session.Send(segment);
            }
        }

        public ServerSession Generate()  // session 생성
        {
            lock (_lock)
            {
                ServerSession session = new ServerSession();
                _sessions.Add(session);
                return session;
            }
        }
    }
}​

 

DummyClient 코드 수정
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using ServerCore;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName(); 
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            Connector connector = new Connector();
            connector.Connect(endPoint, () => { return SessionManager.Instance.Generate(); }, 10); // new를 통해 Session을 생성하는 것이 아닌 SessionManager를 통해 생성하도록 수정, 원하는 Client 수를 인자로 넘김

            while (true)
            {
                try
                {
                    SessionManager.Instance.SendForEach(); // 모든 Session들이 Server쪽으로 계속해서 채팅 패킷을 쏘도록
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }

                Thread.Sleep(250); // 0.25초 휴식
            }
        }
    }
}​

 

DummyClient의 PacketHandler 수정
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

class PacketHandler
{
    // C_Chat을 Server에 보낸뒤, Server는 방에 있는 모든 애들에게 S_Chat으로 답장을 주는 부분을 다룬다.
    public static void S_ChatHandler(PacketSession session, IPacket packet)
    {
        S_Chat chatPacket = packet as S_Chat;
        ServerSession serverSession = session as ServerSession;

        Console.WriteLine(chatPacket.chat);
    }
}​

 

PacketFormat Class 수정
(Main에서 PacketManager.Instance.Register(); 를 직접 입력하는 것이 아닌 생성자를 통해 자동으로 생성되도록 수정)
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 등록
        public static string managerFormat =
@"
using ServerCore;
using System;
using System.Collections.Generic;

class PacketManager
{{
    #region Singleton
    static PacketManager _instance = new PacketManager();
    public static PacketManager Instance {{ get {{ return _instance; }} }}
    #endregion

    PacketManager()
    {{
        Register();
    }}

    Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>> _onRecv = new Dictionary<ushort, Action<PacketSession, ArraySegment<byte>>>();
    Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();

    public void Register() 
    {{
{0}
    }}

    public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer)
    {{
        ushort count = 0;
        ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
        count += 2;
        ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
        count += 2;

        Action<PacketSession, ArraySegment<byte>> action = null;
        if (_onRecv.TryGetValue(id, out action))
            action.Invoke(session, buffer);
    }}

    void MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
    {{
        T packet = new T();
        packet.Read(buffer);

        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(packet.Protocol, out action))
            action.Invoke(session, packet);
    }}
}}
";

        // ...
    }
}​

 

- 위의 결과로 GameRoom 안에 존재하는 10명의 유저들에게 채팅 패킷을 뿌려주고 있다는 것을 알 수 있다.

 ~>  그러나 위와 같은 방식을 MMORPG에 도입할 경우 속도가 상당히 느려질 수 있다.

       (유저의 수가 증가할수록, 패킷을 뿌리는 양이 많아지므로)

- 위의 Thread들은 모두 Broadcast의 lock 부분에서 대기중이다.

 ~> 이는 당연한 결과이다. 왜냐하면 Thread.Sleep(250) 을 통해 0.25초에 1번 동작하도록 설정하였으므로 만약 100명의 

      유저가 있다고 가정하면 100 * 100 = 10,000번이므로 1초에 40,000번 동작하고 있기 때문이다. (250 * 4 = 1000 = 1초)

      이에 lock 부분에 동시다발적으로 수많은 Thread들이 들어오지만 lock 때문에 1번에 1개의 Thread만 처리할 수 있다.

 ~> 따라서 위의 수많은 작업들이 밀리게 되면서 Thread를 관리하는 입장에서는 Thread를 보냈으나 일 처리가 완료되지 

      않았기 때문에 다시 Thread를 보내고 있는 상황이 발생한다. 이에 Thread가 계속해서 쌓이게 되는 것이다.

- 이러한 문제가 발생하는 이유는 Recv를 하자마자, lock을 통해 패킷을 전송하였기 때문이다.

 ~> 해결 방법으로는 하나의 Thread만 Queue에 쌓여있는 일감을 처리하고, 다른 Thread들의 일감은 Queue에 담아두는

      것이다. (이를 Job 또는 Task라고 하며, 중요한 것은 패킷을 저장하고 하나의 Thread에서 처리하는 것)

 

# Command 패턴

- 다음 시간에 만들어볼 Job 또는 Task를 관리하는 Queue가 전형적인 Command 패턴의 예제이다.

 

- 손님을 대리하는 ClientSession이 직원인 Thread에게 주문을 한다.

 

 

- 만약 직원이 서빙, 요리, 계산을 전부 도맡아 할 경우 주문을 받자마자 주방에 달려가 요리를 바로 시작할 것이다.

 ~> 지금까지 구현된 코드가 위와 비슷하다.

 

 

- 만약 주방의 크기가 너무 작아 동시에 1명만 요리가 가능한 경우 주문을 받은 직원들은 주방 앞에서 자신의 요리 차례가

  올때까지 계속해서 기다리게 된다.

 

 

- 모든 직원들이 자신의 요리 차례를 기다리고 있기 때문에 주문을 받을 직원이 부족한 경우 식당은 직원을 더 고용한다.

 ~> 지금까지 구현된 코드의 결과가 위와 비슷하다.

 

 

- 위와 같이 1명의 직원이 서빙, 요리, 계산을 모두 담당하는 것이 아닌 직원들 각각이 업무를 분담받도록 한다.

 ~> 서빙을 담당하는 직원이 주문을 받아 주문서를 주방장에게 전달한다.

 ~> 이러한 방법이 command 패턴과 유사하다.

 

+ 추가 검색 (https://velog.io/@kyeun95/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%BB%A4%EB%A7%A8%EB%93%9C-%ED%8C%A8%ED%84%B4Command-Pattern)

- command 패턴이란?

 ~> command 패턴은 객체 지향 디자인 패턴 중 하나로, 객체 간의 결합도를 낮추고 유연성을 높이는 패턴이다.

 ~> command 패턴의 주요 목적은 사용자가 보낸 요청을 객체의 형태로 캡슐화하여 이를 나중에 이용할 수 있도록

     이름, 매개변수 등 요청에 필요한 정보를 저장 또는 로깅, 취소할 수 있도록 하는 패턴으로, 이를 통해 메소드를

     호출하는 Class와 메소드를 구현하는 Class 사이의 결합을 느슨하게 만든다.

 ~> 따라서 Client가 요청의 수신자를 알 필요 없이 다양한 요청을 보낼 수 있게 된다.

 

+ 추가 검색 (https://bamtory29.tistory.com/entry/%EC%BB%A4%EB%A7%A8%EB%93%9C-%ED%8C%A8%ED%84%B4-Command-Pattern)

Command 인터페이스
public interface Command 
{
    public void execute();
}​

 

ComputerOnCommand Class
public class ComputerOnCommand implements Command
{
    private Computer computer;

    public ComputerOnCommand(Computer computer) 
    {
        this.computer = computer;
    }
    
    @Override
    public void execute() 
    {
        computer.turnOn();
    }
}​

 

ComputerOffCommand Class
public class ComputerOffCommand implements Command 
{
    private Computer computer;

    public ComputerOffCommand(Computer computer) 
    {
        this.computer = computer;
    }
    
    @Override
    public void execute() 
    {
        computer.turnOff();
    }
}​

 

Computer Class
public class Computer 
{
    public void Computer() {}

    public void turnOn() 
    {
        System.out.println("컴퓨터 전원 켜짐");
    }

    public void turnOff() 
    {
        System.out.println("컴퓨터 전원 꺼짐");
    }
}​

 

Button Class
public class Button 
{
    private Command command;

    public Button(Command command) 
    {
        this.command = command;
    }

    public void setCommand(Command command) 
    {
        this.command = command;
    }

    public void pressButton() 
    {
        this.command.execute();
    }
}​

 

Main 메소드
public static void main(String[] args) 
{
        Computer computer = new Computer(); //컴퓨터는 Receiver

	    //컴퓨터 객체 생성
        ComputerOnCommand computerOnCmd = new ComputerOnCommand(computer);
        ComputerOffCommand computerOffCmd = new ComputerOffCommand(computer);

        Button btn = new Button(computerOnCmd); //버튼이 Invoker 역할
        btn.pressButton();
        btn.setCommand(computerOffCmd);
        btn.pressButton();
}

 

# JobQueue #1

- 지난 시간 학습한 Comman 패턴을 활용하여 JobQueue를 구현할 예정이다.

 ~> Queue에 쌓인 일감을 처리하는 Thread는 Push시 _jobQueue에 처음으로 일감을 밀어 넣는 Thread가 담당한다.

ServerCore에 JobQueue Class 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public interface IJobQueue
    {
        void Push(Action job);
    }

    public class JobQueue : IJobQueue // IJobQueue를 상속 받는다.
    {
        Queue<Action> _jobQueue = new Queue<Action>(); // 일감 목록을 담는 곳
        object _lock = new object(); // MultiThread 환경을 위한 lock 선언
        bool _flush = false; // Queue에 쌓인 일감들을 본인이 처리할 것인지 아닌지

        public void Push(Action job)  // _jobQueue에 일감을 밀어 넣기 위한 함수
        {
            bool flush = false; // MultiThread 환경에서 단 1개의 Thread만 일감 처리를 담당하도록
            lock (_lock)
            {
                _jobQueue.Enqueue(job);
                if (_flush == false) // _flush가 false인 경우 Queue에 쌓인 일감들을 본인이 처리
                    flush = _flush = true;
            }

            if (flush)
                Flush();
        }

        void Flush()
        {
            while (true)
            {
                Action action = Pop();
                if (action == null)
                    return;

                action.Invoke();
            }
        }

        Action Pop() // 일감 처리를 위해 _jobQueue에서 일감을 꺼내기 위한 함수
        {
            lock (_lock)
            {
                if (_jobQueue.Count == 0)
                {
                    _flush = false;
                    return null;
                }

                return _jobQueue.Dequeue();
            }
        }
    }
}​

 

GameRoom Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace Server
{
    class GameRoom : IJobQueue // IJobQueue를 상속 받는다.
    {
        // ...
        
        JobQueue _jobQueue = new JobQueue();

        public void Push(Action job)
        {
            _jobQueue.Push(job);
        }

        // ...
    }
}​

 

ClientSession Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Server
{
    class ClientSession : PacketSession 
    {
        // ...

        public override void OnConnected(EndPoint endPoint)
        {
            // ...

            // 바로 실행하는 것이 아닌 일감을 _jobQueue에 담는 것
            Program.Room.Push(() => Program.Room.Enter(this));
            // Program.Room.Enter(this); // Client가 접속시 방에 입장시킨다. (Program 산하에 static으로 Room을 생성하였기 때문에 다음과 같이 호출)

            // ...
        }

        // ...

        public override void OnDisconnected(EndPoint endPoint)
        {
            // ...
            
            if (Room != null)
            {
                // 실행 도중 Client 종료시 Null Crash 방지를 위한 것 & 바로 실행하는 것이 아닌 일감을 _jobQueue에 담는 것
                GameRoom room = Room;
                room.Push(() => room.Leave(this));
                //Room.Leave(this);
                Room = null;
            }

            // ...
        }

        // ...
    }
}​

 

PacketHandler Class 수정
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

class PacketHandler 
{
    public static void C_ChatHandler(PacketSession session, IPacket packet)
    {
        // ...

        // 실행 도중 Client 종료시 Null Crash 방지를 위한 것 & 바로 실행하는 것이 아닌 일감을 _jobQueue에 담는 것
        GameRoom room = clientSession.Room;
        room.Push(() => room.Broadcast(clientSession, chatPacket.chat));
        // clientSession.Room.Broadcast(clientSession, chatPacket.chat); // 현재 clientSession이 접속중인 방에 존재하는 모두에게 채팅 메시지를 뿌린다.
    }
}​

 

# JobQueue #2

- 지난 시간에 구현한 JobQueue와 똑같이 동작하지만 수동적으로 Task를 만들어주는 방법에 대해 알아볼 예정이다.

 ~> 람다식 개념이 등장한지 얼마 되지 않아 현업과 같은 실무에서는 수동적으로 일 처리가 필요한 함수들을 구현해서

      처리하는 형식이 자주 사용된다.

 ~> 그러나 위와 같이 수동적인 방법은 일 처리가 필요한 함수를 모두 구현해야 한다는 단점이 존재한다.

Server에 TaskQueue Class 생성
(_queue에 쌓인 일감 처리는 지난 시간의 Flush() 와 같은 메소드에서 처리하도록)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Server
{
    interface ITask
    {
        void Execute();
    }

    // 일 처리가 필요한 함수에 맞춰 클래스를 생성
    class BroadcastTask : ITask
    {
        // 함수 실행시 필요한 변수들을 선언
        GameRoom _room;
        ClientSession _session;
        string _chat;

        // 생성자를 통한 변수 초기화
        BroadcastTask(GameRoom room, ClientSession session, string chat)
        {
            _room = room;
            _session = session;
            _chat = chat;
        }

        public void Execute()
        {
            _room.Broadcast(_session, _chat);
        }
    }

    class TaskQueue
    {
        Queue<ITask> _queue = new Queue<ITask>();
    }
}​

 

부하 Test를 위한 Listener Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    public class Listener
    {
        Socket _listenSocket;

        Func<Session> _sessionFactory;

        // 문지기 수를 10명으로 증가, 최대 대기수를 100명으로 증원
        public void Init(IPEndPoint endPoint, Func<Session> sessionFactory, int register = 10, int backlog = 100)
        {
            // 문지기 고용
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            _sessionFactory += sessionFactory;

            // 문지기 교육
            _listenSocket.Bind(endPoint);

            // 영업 시작
            // backlog : 최대 대기 수
            _listenSocket.Listen(backlog);

            for (int i = 0; i < register; i++)
            {
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);
                RegisterAccept(args);
            }
        }

        // ...
    }
}​

 

- 만약 Client 접속 인원을 500명으로 늘린 뒤에 실행할 경우 메모리가 계속해서 상승하는 것을 볼 수 있다.

 ~> 이는 수많은 작업들이 밀리게 되면서 Thread를 관리하는 입장에서는 Thread를 보냈으나 일 처리가 완료되지 않았기

      때문에 ThreadPool에서 새로운 Thread를 뽑아 보내고 있기 때문에 메모리가 계속해서 상승하는 것이다.

- 가장 심한 부하를 유발하는 곳은 Broadcast의 foreach문 내의 Send이다.

 ~> 왜냐하면 Thread.Sleep(250) 을 통해 0.25초에 1번 동작하도록 설정하였으므로 500명의 유저가 있다고 가정하면

      500 * 500 = 250,000번이므로 1초에 1,000,000번 동작하고 있기 때문이다. 

 ~> 이는 N^2의 시간 복잡도를 가진다.

 ~> 해결 방법으로는 패킷 요청이 올때마다 바로 Send 하는 것이 아닌 패킷을 모은 뒤 추후에 한번에 보내는 것이다.

 

+ 추가 검색 (https://shung2.tistory.com/1445)

- Zone 형식의 게임은 맵을 넘어갈 때 로딩 시간이 있기때문에 하나의 Zone 단위에 JobQueue를 넣어 처리하도록 만들면

 되므로 비교적 JobQueue 활용 처리가 용이하다.

- 그러나 Seamless 게임은 넓은 맵에서 임의로 영역을 구분하여 JobQueue를 두고 처리할 수는 있겠지만, 이렇게 된다면

  구분된 영역 사이에서는 어떻게 처리할 지에 대한 의문점이 생길 수 밖에 없다.

 ~> JobQueue를 모든 객체들에게 넣는 방법이 있다. (즉, User, Monster, Skill ...)

 

+ 추가 검색 (https://drehzr.tistory.com/1683)

 

# 패킷 모아 보내기

- 패킷을 모아 보내는 것은 Engine 쪽에서도 처리할 수 있고 Content 쪽에서도 처리할 수 있다.

 ~> 만약 Engine 쪽에서 처리하고자 할 경우 Session의 Send() 메소드에서 처리가 가능하다.

      (어느정도 쌓아둔 뒤 일정 조건에 따라 보내는 구조로 변경)

 ~> 그러나 이번 시간에는 Content 쪽에서 패킷 모아 보내기를 구현할 예정이다.

 ~> 패킷이 불규칙하게 전송될 경우 RecvBuffer의 크기를 늘려주면 된다.

Session Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    // ...

    public abstract class Session
    {
        // ...
        
        // List<ArraySegment<byte>> 를 매개변수로 받아 List에 담긴 패킷들을 전부 처리하는 Send 메소드 추가
        public void Send(List<ArraySegment<byte>> sendBuffList)
        {
            if (sendBuffList.Count == 0)
                return;

            lock (_lock)
            {
                foreach (ArraySegment<byte> sendBuff in sendBuffList)
                    _sendQueue.Enqueue(sendBuff);

                if (_pendingList.Count == 0)
                    RegisterSend();
            }
        }
        
        // 기존의 Send 메소드
        public void Send(ArraySegment<byte> sendBuff)
        {
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);

                if (_pendingList.Count == 0)
                    RegisterSend();
            }
        }

        // ...
    }
}​

 

GameRoom Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace Server
{
    class GameRoom : IJobQueue 
    {
        // ...
        
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>(); // 패킷을 모으기 위한 List

        // ...

        public void Flush() // 모아둔 패킷을 처리
        {
            foreach (ClientSession s in _sessions)
                s.Send(_pendingList);

            Console.WriteLine($"Flushed {_pendingList.Count} items");
            _pendingList.Clear();
        }

        // ..
    }
}​

 

Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Program
    {
        // ...

        static void Main(string[] args)
        {
            // ...

            while (true)
            {
                Room.Push(() => Room.Flush());
                Thread.Sleep(250);
            }
        }
    }
}​

 

# JobTimer

- GameRoom 뿐만이 아닌 다양한 Room들이 추가될 경우 각각의 룸들은 서로 다른 대기시간을 가지며 실행된다.

 ~> 첫번째로는 Tick을 이용하는 방법이 있다.

 ~> 두번째로는 PriorityQueue를 이용하는 방법이 있다.

Tick을 통해 처리하기 위한 Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Program
    {
        // ...

        static void Main(string[] args)
        {
            // ...

            int roomTick = 0;
            // room이 추가될때마다 Tick 변수도 추가된다.
            while (true)
            {
                int now = System.Environment.TickCount;
                if (roomTick < now) // 그러나 이와 같은 방법은 불필요한 if문 체크가 반복된다.
                {
                    Room.Push(() => Room.Flush());
                    roomTick = now + 250;
                }
                // room이 추가될때마다 if문도 추가된다.
            }
        }
    }
}​

 

ServerCore에 PriorityQueue Class 생성
using System;
using System.Collections.Generic;
using System.Text;

namespace ServerCore
{
    public class PriorityQueue<T> where T : IComparable<T>
    {
        List<T> _heap = new List<T>();

        public int Count { get { return _heap.Count; } }

        // 0 (logN)
        public void Push(T data)
        {
            // 힙의 맨 끝에 새로운 데이터를 삽입한다.
            _heap.Add(data);

            int now = _heap.Count - 1;
            // 도장깨기를 시작
            while (now > 0)
            {
                // 도장깨기를 시도
                int next = (now - 1) / 2;
                if (_heap[now].CompareTo(_heap[next]) < 0) // 대상값이 비교값과 같은 경우 0, 작은 경우 -1, 큰 경우 1 ( 대상값.CompareTo(비교값) )
                    break; // 실패

                // 두 값을 교체한다.
                T temp = _heap[now];
                _heap[now] = _heap[next];
                _heap[next] = temp;

                // 검사 위치를 이동한다.
                now = next;
            }
        }

        // 0 (logN)
        public T Pop()
        {
            // 반환할 데이터를 따로 저장
            T ret = _heap[0];

            // 마지막 데이터를 루트로 이동한다.
            int lastIndex = _heap.Count - 1;
            _heap[0] = _heap[lastIndex];
            _heap.RemoveAt(lastIndex);
            lastIndex--;

            // 역으로 내려가는 도장깨기 시작
            int now = 0;
            while (true)
            {
                int left = 2 * now + 1;
                int right = 2 * now + 2;

                int next = now;
                // 왼쪽값이 현재값보다 크면, 왼쪽으로 이동
                if (left <= lastIndex && _heap[next].CompareTo(_heap[left]) < 0)
                    next = left;
                // 오른쪽값이 현재값(왼쪽 이동 포함)보다 크면, 오른쪽으로 이동
                if (right <= lastIndex && _heap[next].CompareTo(_heap[right]) < 0)
                    next = right;

                // 왼쪽/오른쪽 모두 현재값보다 작으면 종료
                if (next == now)
                    break;

                // 두 값을 교체한다.
                T temp = _heap[now];
                _heap[now] = _heap[next];
                _heap[next] = temp;
                // 검사 위치를 이동한다.
                now = next;
            }

            return ret;
        }

        public T Peek() // 일감 내용 확인을 위한 함수
        {
            if (_heap.Count == 0)
                return default(T);
            return _heap[0];
        }
    }
}​

 

Server에 JobTimer Class 생성
using System;
using System.Collections.Generic;
using System.Text;
using ServerCore;

namespace Server
{
    struct JobTimerElem : IComparable<JobTimerElem> // 하나의 일감 단위
    {
        public int execTick; // 실행 시간
        public Action action; // 일감 (즉, Job)

        // IComparable 인터페이스의 필수 구성 요소인 CompareTo()
        public int CompareTo(JobTimerElem other)
        {
            // 실행 시간이 적을수록 먼저 튀어나오도록 (즉, 우선순위가 높은 항목)
            return other.execTick - execTick;
        }
    }

    class JobTimer // Job을 예약할 수 있는 시스템
    {
        // 우선순위 큐는 대소관계 연산 처리 속도가 상당히 빠르다.
        PriorityQueue<JobTimerElem> _pq = new PriorityQueue<JobTimerElem>();
        object _lock = new object(); // MultiThread 환경을 위한 lock 선언

        public static JobTimer Instance { get; } = new JobTimer();

        public void Push(Action action, int tickAfter = 0) // 예약하고자 하는 일감, 몇 Tick 후에 실행해야하는지 (입력 받지 못한 경우 바로 실행하도록 0으로 설정)
        {
            JobTimerElem job;
            job.execTick = System.Environment.TickCount + tickAfter;
            job.action = action;

            lock (_lock)
            {
                _pq.Push(job);
            }
        }

        public void Flush() // 일감 처리
        {
            while (true)
            {
                int now = System.Environment.TickCount;

                JobTimerElem job;

                lock (_lock)
                {
                    if (_pq.Count == 0) // 일감이 없는 경우
                        break; // while문을 나간다는 의미

                    job = _pq.Peek(); // 꺼내지 않고 일감 내용만 확인하는 것
                    if (job.execTick > now) // 현재 시간보다 실행 시간이 많이 남은 경우
                        break;

                    _pq.Pop();
                }

                // 일감을 실행시킨다.
                job.action.Invoke();
            }
        }
    }
}​

 

Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Program
    {
        // ...
        
        static void FlushRoom()
        {
            Room.Push(() => Room.Flush());
            JobTimer.Instance.Push(FlushRoom, 250);
        }

        static void Main(string[] args)
        {
            // ...

            JobTimer.Instance.Push(FlushRoom);

            while (true)
            {
                JobTimer.Instance.Flush();
            }
        }
    }
}​

 

+ 추가 검색 (https://velog.io/@aenyoung/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%9A%B0%EC%84%A0%EC%88%9C%EC%9C%84-%ED%81%90%EC%99%80-%ED%9E%99)

- Priority Queue란? (즉, 우선순위 큐란?) 

 ~> 선입선출(FIFO)의 원칙에 의하여 먼저 들어온 데이터가 먼저 나가는 Queue와 달리 Priority Queue는 데이터들이

      우선순위를 가지고 있어 우선순위가 높은 데이터가 먼저 출력되는 자료구조이다.

 

[ 섹션 5. 유니티 연동 ]

# 유니티 연동 #1

- 유니티 연동 전 좋은 소식과 나쁜 소식이 존재한다.

 ~> 좋은 소식은 네트워크 통신을 위해 구현한 코드들을 어느정도 재사용할 수 있다는 것이다.

 ~> 나쁜 소식은 Unity의 정책상 C#에서 허용되는 문법과 허용되지 않는 문법들이 존재하기 때문에 GenPacket의 

      Span, ReadOnlySpan, TryWriteBytes 와 같은 것들을 사용할 수 없다는 것이다.

      (Unity 2021 버전 이후부터는 Engine에서 지원하지만 오류가 난다는 가정하에 강의를 들을 예정)

- 또한 Unity에서 MultiThread 환경은 제약사항이 존재한다.

 ~> Main Thread가 아닌 Background Thread에서 Unity가 관리하는 객체들을 접근하거나 코드를 실행하려고 하는 경우

      Crash가 발생한다.

 ~> 따라서 Game Logic은 Main Thread 에서만 접근 및 실행하도록 조정해야 한다.

 

- 우선 Unity Hub를 통해 새로운 Client 프로젝트를 생성한 뒤 Scripts 폴더 산하에 재사용하고자 하는 코드들을 복붙한다.

 ~> 프로젝트의 경로는 여태까지 실습하던 프로젝트의 경로로 설정

PacketFormat Class 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // ...

        // {0} 패킷 이름
        // {1} 멤버 변수들
        // {2} 멤버 변수 Read
        // {3} 멤버 변수 Write
        public static string packetFormat =
@"
public class {0} : IPacket
{{
	{1}

	public ushort Protocol {{ get {{ return (ushort)PacketID.{0}; }} }}

	public void Read(ArraySegment<byte> segment)
	{{
		ushort count = 0;
		count += sizeof(ushort);
		count += sizeof(ushort);
		{2}
	}}

	public ArraySegment<byte> Write()
	{{
		ArraySegment<byte> segment = SendBufferHelper.Open(4096);
		ushort count = 0;

		count += sizeof(ushort);
		Array.Copy(BitConverter.GetBytes((ushort)PacketID.{0}), 0, segment.Array, segment.Offset + count, sizeof(ushort));
		count += sizeof(ushort);
		{3}

		Array.Copy(BitConverter.GetBytes(count), 0, segment.Array, segment.Offset, sizeof(ushort));

		return SendBufferHelper.Close(count);
	}}
}}
";
        // ...

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        // {2} 멤버 변수들
        // {3} 멤버 변수 Read
        // {4} 멤버 변수 Write
        public static string memberListFormat =
@"public class {0}
{{
	{2}

	public void Read(ArraySegment<byte> segment, ref ushort count)
	{{
		{3}
	}}

	public bool Write(ArraySegment<byte> segment, ref ushort count)
	{{
		bool success = true;
		{4}
		return success;
	}}	
}}
public List<{0}> {1}s = new List<{0}>();";

        // {0} 변수 이름
        // {1} To~ 변수 형식
        // {2} 변수 형식
        public static string readFormat =
@"this.{0} = BitConverter.{1}(segment.Array, segment.Offset + count);
count += sizeof({2});";

        // ...

        // {0} 변수 이름
        public static string readStringFormat =
@"ushort {0}Len = BitConverter.ToUInt16(segment.Array, segment.Offset + count);
count += sizeof(ushort);
this.{0} = Encoding.Unicode.GetString(segment.Array, segment.Offset + count, {0}Len);
count += {0}Len;";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string readListFormat =
@"this.{1}s.Clear();
ushort {1}Len = BitConverter.ToUInt16(segment.Array, segment.Offset + count);
count += sizeof(ushort);
for (int i = 0; i < {1}Len; i++)
{{
	{0} {1} = new {0}();
	{1}.Read(segment, ref count);
	{1}s.Add({1});
}}";

        // {0} 변수 이름
        // {1} 변수 형식
        public static string writeFormat =
@"Array.Copy(BitConverter.GetBytes(this.{0}), 0, segment.Array, segment.Offset + count, sizeof({1}));
count += sizeof({1});";

        // ...

        // {0} 변수 이름
        public static string writeStringFormat =
@"ushort {0}Len = (ushort)Encoding.Unicode.GetBytes(this.{0}, 0, this.{0}.Length, segment.Array, segment.Offset + count + sizeof(ushort));
Array.Copy(BitConverter.GetBytes({0}Len), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
count += {0}Len;";

        // {0} 리스트 이름 [대문자]
        // {1} 리스트 이름 [소문자]
        public static string writeListFormat =
@"Array.Copy(BitConverter.GetBytes((ushort)this.{1}s.Count), 0, segment.Array, segment.Offset + count, sizeof(ushort));
count += sizeof(ushort);
foreach ({0} {1} in this.{1}s)
	{1}.Write(segment, ref count);";

    }
}​

 

GenPackets.bat 배치 파일 수정 (배치 파일 실행시 Unity Script 또한 같이 변경되도록)
START ../../PacketGenerator/bin/PacketGenerator.exe ../../PacketGenerator/PDL.xml
XCOPY /Y GenPackets.cs "../../DummyClient/Packet"
XCOPY /Y GenPackets.cs "../../Client/Assets/Scripts/Packet"
XCOPY /Y GenPackets.cs "../../Server/Packet"
XCOPY /Y ClientPacketManager.cs "../../DummyClient/Packet"
XCOPY /Y ClientPacketManager.cs "../../Client/Assets/Scripts/Packet"
XCOPY /Y ServerPacketManager.cs "../../Server/Packet"​

 

Unity Project에 NetworkManager Script 생성후 빈 객체에 Component로 추가
using DummyClient;
using ServerCore;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;

public class NetworkManager : MonoBehaviour
{
    ServerSession _session = new ServerSession();

    void Start()
    {
        // DNS (Domain Name System)
        string host = Dns.GetHostName();
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

        Connector connector = new Connector();

        connector.Connect(endPoint, () => { return _session; }, 1);
    }

    void Update()
    {
        
    }
}​

 

통신이 원활하게 이루어지는지 확인하기 위해 Unity Project의 PacketHandler Script 수정
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

class PacketHandler
{
	public static void S_ChatHandler(PacketSession session, IPacket packet)
	{
		S_Chat chatPacket = packet as S_Chat;
		ServerSession serverSession = session as ServerSession;

        if (chatPacket.playerId == 1)
            Debug.Log(chatPacket.chat);
		//if (chatPacket.playerId == 1)
			//Console.WriteLine(chatPacket.chat);
	}
}​

 

 

- Test를 위해 Server 솔루션 프로젝트를 실행시켜 Server와 Client를 구동시킨 뒤, Unity의 Play 버튼을 누르면

  Console 창에 Log가 뜨는 것을 확인할 수 있다.

 

# 유니티 연동 #2

- 이번 시간에는 단순히 Log만 띄우는 것이 아닌 실질적인 액션을 취하도록 만들 예정이다.

 ~> 우선 Unity에서 [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 3D Object ] - [ Cylinder ] 를 통해 3D 객체 생성 후 이름을

      Player로 설정해준다.

Unity Project의 PacketHandler Script 수정
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

class PacketHandler
{
	public static void S_ChatHandler(PacketSession session, IPacket packet)
	{
        S_Chat chatPacket = packet as S_Chat;
        ServerSession serverSession = session as ServerSession;

        if (chatPacket.playerId == 1)
        {
            Debug.Log(chatPacket.chat);

            // Unity 내의 객체를 찾는 Logic 추가
            GameObject go = GameObject.Find("Player");
            if (go == null)
                Debug.Log("Player not found");
            else
                Debug.Log("Player found");
        }
    }
}​

 

- 위의 추가한 Logic은 정상적으로 실행되지 않는다.

 ~> 기존의 우리가 작성한 Server 솔루션의 Logic은 비동기로 네트워크 통신을 하고 있다.

 ~> 따라서 Unity를 구동하는 Main Thread에서 네트워크 패킷을 실행하는 것이 아닌, Thread Pool에서 Thread를 꺼내와

      실행하고 있는 것이 문제가 된다.

 ~> Unity는 다른 Thread에서 게임과 관련된 부분을 접근하여 실행하는 것을 원천적으로 차단해 두었기 때문에 

      정상적으로 실행되지 않는 것이다.

 ~> 해결 방법으로는 PacketHandler Class가 Main Thread에서 실행되도록 만들면 된다.

      (S_ChatHandler에서 Logic을 처리하는 것이 아닌 Queue에 일감을 등록후, 처리하는 Logic을 구분 지어 사용)

Unity Project의 GenPacket Script 수정
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;

// ...

public interface IPacket // 접근한정자 public 추가
{
	ushort Protocol { get; }
	void Read(ArraySegment<byte> segment);
	ArraySegment<byte> Write();
}

// ...​

 

Unity Project에 NetworkManager Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// Main Thread와 Background Thread (즉, 네트워크를 처리하는 애들끼리) 는 PacketQueue라는 통로를 통해 소통한다.
//  ~> Background Thread는 Pakcet을 Push하여 밀어 넣고, Main Thread에서는 Packet을 Pop하여 처리한다.
public class PacketQueue // Component로 사용할 건 X ~> MonoBehaviour 상속 X
{
    public static PacketQueue Instance { get; } = new PacketQueue();

    Queue<IPacket> _packetQueue = new Queue<IPacket>();
    object _lock = new object();

    public void Push(IPacket packet)
    {
        lock (_lock)
        {
            _packetQueue.Enqueue(packet);
        }
    }

    public IPacket Pop()
    {
        lock ( _lock)
        {
            if (_packetQueue.Count == 0)
                return null;

            return _packetQueue.Dequeue();
        }
    }
}​

 

일감 등록을 위한 Unity Project의 ClientPacketManager Script 수정
using ServerCore;
using System;
using System.Collections.Generic;

class PacketManager
{
    #region Singleton
    static PacketManager _instance = new PacketManager();
    public static PacketManager Instance { get { return _instance; } }
    #endregion

    PacketManager()
    {
        Register();
    }

    Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();
    Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
		
    public void Register()
    {
        _makeFunc.Add((ushort)PacketID.S_Chat, MakePacket<S_Chat>);
        _handler.Add((ushort)PacketID.S_Chat, PacketHandler.S_ChatHandler);
    }

    // Action<PacketSession, IPacket> Type의 매개변수인 onRecvCallback 을 추가로 입력 받는다.
    public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer, Action<PacketSession, IPacket> onRecvCallback = null)
    {
        ushort count = 0;

        ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
        count += 2;
        ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
        count += 2;

        Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
        if (_makeFunc.TryGetValue(id, out func))
        {
            IPacket packet = func.Invoke(session, buffer);
            if (onRecvCallback != null)
                onRecvCallback.Invoke(session, packet);
            else
                HandlePacket(session, packet);
        }
    }

    T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
    {
        T pkt = new T();
        pkt.Read(buffer);
        return pkt;
    }

    public void HandlePacket(PacketSession session, IPacket packet)
    {
        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(packet.Protocol, out action))
            action.Invoke(session, packet);
    }
}​

 

일감 등록을 위한 Unity Project의 ServerSession Script 수정
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;

namespace DummyClient
{
    class ServerSession : PacketSession
    {
        // ...

        // Action<PacketSession, IPacket> Type의 매개변수인 onRecvCallback 을 추가로 입력
        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            PacketManager.Instance.OnRecvPacket(this, buffer, (s, p) => PacketQueue.Instance.Push(p));
        }

        // ...
    }
}​

 

일감 처리를 위한 Unity Project의 NetworkManager Script 수정
using DummyClient;
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;

public class NetworkManager : MonoBehaviour
{
    ServerSession _session = new ServerSession();

    void Start()
    {
        // DNS (Domain Name System)
        string host = Dns.GetHostName();
        IPHostEntry ipHost = Dns.GetHostEntry(host);
        IPAddress ipAddr = ipHost.AddressList[0];
        IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

        Connector connector = new Connector();

        connector.Connect(endPoint,
            () => { return _session; }, 1);

        StartCoroutine("CoSendPacket");
    }

    void Update()
    {
        // 일감 처리
        IPacket packet = PacketQueue.Instance.Pop();
        if (packet != null)
        {
            PacketManager.Instance.HandlePacket(_session, packet);
        }
    }

    // 3초마다 패킷을 보내도록 (즉, DummyClient의 역할)
    IEnumerator CoSendPacket()
    {
        while (true)
        {
            yield return new WaitForSeconds(3.0f);

            C_Chat chatPacket = new C_Chat();
            chatPacket.chat = "Hello Unity !";
            ArraySegment<byte> segment = chatPacket.Write();

            _session.Send(segment);
        }
    }
}​

 

PacketFormat Class 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace PacketGenerator
{
    class PacketFormat
    {
        // {0} 패킷 등록
        public static string managerFormat =
@"using ServerCore;
using System;
using System.Collections.Generic;

public class PacketManager
{{
	#region Singleton
	static PacketManager _instance = new PacketManager();
	public static PacketManager Instance {{ get {{ return _instance; }} }}
	#endregion

	PacketManager()
	{{
		Register();
	}}

	Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>> _makeFunc = new Dictionary<ushort, Func<PacketSession, ArraySegment<byte>, IPacket>>();
	Dictionary<ushort, Action<PacketSession, IPacket>> _handler = new Dictionary<ushort, Action<PacketSession, IPacket>>();
		
	public void Register()
	{{
{0}
	}}

	public void OnRecvPacket(PacketSession session, ArraySegment<byte> buffer, Action<PacketSession, IPacket> onRecvCallback = null)
	{{
		ushort count = 0;

		ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
		count += 2;
		ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + count);
		count += 2;

		Func<PacketSession, ArraySegment<byte>, IPacket> func = null;
		if (_makeFunc.TryGetValue(id, out func))
		{{
            IPacket packet = func.Invoke(session, buffer);
			if (onRecvCallback != null)
				onRecvCallback.Invoke(session, packet);
			else
				HandlePacket(session, packet);
        }}
	}}

	T MakePacket<T>(PacketSession session, ArraySegment<byte> buffer) where T : IPacket, new()
	{{
		T pkt = new T();
		pkt.Read(buffer);
		return pkt;
	}}

	public void HandlePacket(PacketSession session, IPacket packet)
	{{
        Action<PacketSession, IPacket> action = null;
        if (_handler.TryGetValue(packet.Protocol, out action))
            action.Invoke(session, packet);
    }}
}}";

        // {0} 패킷 이름
        public static string managerRegisterFormat =
@"		_makeFunc.Add((ushort)PacketID.{0}, MakePacket<{0}>);
		_handler.Add((ushort)PacketID.{0}, PacketHandler.{0}Handler);";

        // {0} 패킷 이름/번호 목록
        // {1} 패킷 목록
        public static string fileFormat =
@"using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using ServerCore;

public enum PacketID
{{
	{0}
}}

public interface IPacket
{{
	ushort Protocol {{ get; }}
	void Read(ArraySegment<byte> segment);
	ArraySegment<byte> Write();
}}

{1}
";

        // ...
    }
}​

 

# 유니티 연동 #3

- 이번 시간과 다음 시간을 통해 Server에서의 Player 생성 및 움직임을 구현 예정이다.

 ~> 우선 Server Logic 수정 후, DummyClient Logic을 수정할 것이다.

PDL XML 파일 수정
<?xml version="1.0" encoding="utf-8" ?>
<PDL>
	<packet name="S_BroadcastEnterGame">
		<int name="playerId"/>
		<float name="posX"/>
		<float name="posY"/>
		<float name="posZ"/>
	</packet>
	<packet name="C_LeaveGame">
	</packet>
	<packet name="S_BroadcastLeaveGame">
		<int name="playerId"/>
	</packet>
	<packet name="S_PlayerList">
		<list name="player">
			<bool name="isSelf"/>
			<int name="playerId"/>
			<float name="posX"/>
			<float name="posY"/>
			<float name="posZ"/>
		</list>
	</packet>
	<packet name="C_Move">
		<float name="posX"/>
		<float name="posY"/>
		<float name="posZ"/>
	</packet>
	<packet name="S_BroadcastMove">
		<int name="playerId"/>
		<float name="posX"/>
		<float name="posY"/>
		<float name="posZ"/>
	</packet>
</PDL>​

 

ClientSession Class 수정
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;
using System.Net;

namespace Server
{
    class ClientSession : PacketSession
    {
        public int SessionId { get; set; }
        public GameRoom Room { get; set; }
        public float PosX { get; set; } // x 좌표 값 저장을 위해 추가
        public float PosY { get; set; } // y 좌표 값 저장을 위해 추가
        public float PosZ { get; set; } // z 좌표 값 저장을 위해 추가

        // ...
    }
}

 

GameRoom Class 수정
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

namespace Server
{
    class GameRoom : IJobQueue
    {
        // ...

        public void Broadcast(ArraySegment<byte> segment)
        {
            _pendingList.Add(segment);		
        }

        public void Enter(ClientSession session)
        {
            // 플레이어 추가
            _sessions.Add(session);
            session.Room = this;

            // 신입생한테 모든 플레이어 목록 전송
            S_PlayerList players = new S_PlayerList();
            foreach (ClientSession s in _sessions)
            {
                players.players.Add(new S_PlayerList.Player()
                {
                    isSelf = (s == session),
                    playerId = s.SessionId,
                    posX = s.PosX,
                    posY = s.PosY,
                    posZ = s.PosZ
                });
            }
            session.Send(players.Write());

            // 신입생 입장을 모두에게 알린다
            S_BroadcastEnterGame enter = new S_BroadcastEnterGame();
            enter.playerId = session.SessionId;
            enter.posX = 0;
            enter.posY = 0;
            enter.posZ = 0;
            Broadcast(enter.Write());
        }

        public void Leave(ClientSession session)
        {
            // 플레이어 제거
            _sessions.Remove(session);

            // 플레이어 퇴장을 모두에게 알린다
            S_BroadcastLeaveGame leave = new S_BroadcastLeaveGame();
            leave.playerId = session.SessionId;
            Broadcast(leave.Write());
        }

        public void Move(ClientSession session, C_Move packet)
        {
            // 좌표를 바꿔주고
            session.PosX = packet.posX;
            session.PosY = packet.posY;
            session.PosZ = packet.posZ;

            // 모두에게 알린다
            S_BroadcastMove move = new S_BroadcastMove();
            move.playerId = session.SessionId;
            move.posX = session.PosX;
            move.posY = session.PosY;
            move.posZ = session.PosZ;
            Broadcast(move.Write());
        }
    }
}​

 

Server의 PacketHandler 수정
using Server;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

class PacketHandler
{
    public static void C_LeaveGameHandler(PacketSession session, IPacket packet)
    {
        C_LeaveGame chatPacket = packet as C_LeaveGame;
        ClientSession clientSession = session as ClientSession;

        if (clientSession.Room == null)
            return;

        GameRoom room = clientSession.Room;
        room.Push(() => room.Leave(clientSession));
    }

    public static void C_MoveHandler(PacketSession session, IPacket packet)
    {
        C_Move movePacket = packet as C_Move;
        ClientSession clientSession = session as ClientSession;

        if (clientSession.Room == null)
            return;

        GameRoom room = clientSession.Room;
        room.Push(() => room.Move(clientSession, movePacket));
    }
}​

 

DummyClient의 PacketHandler 수정
(빌드가 통과할 수 있도록 함수만 만들어줄 뿐, 실질적인 작업은 유니티 내부에서 처리)
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;

class PacketHandler
{
    public static void S_BroadcastEnterGameHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastEnterGame pkt = packet as S_BroadcastEnterGame;
        ServerSession serverSession = session as ServerSession;
    }

    public static void S_BroadcastLeaveGameHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastLeaveGame pkt = packet as S_BroadcastLeaveGame;
        ServerSession serverSession = session as ServerSession;
    }

    public static void S_PlayerListHandler(PacketSession session, IPacket packet)
    {
        S_PlayerList pkt = packet as S_PlayerList;
        ServerSession serverSession = session as ServerSession;
    }

    public static void S_BroadcastMoveHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastMove pkt = packet as S_BroadcastMove;
        ServerSession serverSession = session as ServerSession;
    }
}​

 

DummyClient의 SessionManager 수정
using System;
using System.Collections.Generic;
using System.Text;

namespace DummyClient
{
    class SessionManager
    {
        // ...
        
        Random _rand = new Random();

        public void SendForEach()
        {
            lock (_lock)
            {
                // 채팅 패킷이 아닌 이동 패킷을 보내도록 수정
                foreach (ServerSession session in _sessions)
                {
                    C_Move movePacket = new C_Move();
                    movePacket.posX = _rand.Next(-50, 50);
                    movePacket.posY = 0;
                    movePacket.posZ = _rand.Next(-50, 50);
                    session.Send(movePacket.Write());
                }
            }
        }

        // ...
    }
}

 

# 유니티 연동 #4

- 지난 시간에 이어 Server에서의 Player 생성 및 움직임을 마저 구현할 예정이다.

Unity Project의 PacketQueue Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PacketQueue
{
    // ...

    public List<IPacket> PopAll()
    {
        List<IPacket> list = new List<IPacket>();

        lock (_lock) 
        {
            while (_packetQueue.Count > 0)
                list.Add(_packetQueue.Dequeue());
        }

        return list;
    }
}​

 

Unity Project의 NetworkManager Script 수정
using DummyClient;
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using UnityEngine;

public class NetworkManager : MonoBehaviour
{
    // ...
    
    public void Send(ArraySegment<byte> sendBuff)
    {
        _session.Send(sendBuff);
    }
    
    // ...

    void Update()
    {
        // 일감 처리
        // 프레임마다 1개의 일감만을 처리하는 것이 아닌, 프레임마다 모든 일감하도록 수정
        List<IPacket> list = PacketQueue.Instance.PopAll(); // 즉, Pop()이 아닌 PopAll()을 실행하도록 수정
        foreach (IPacket packet in list)
            PacketManager.Instance.HandlePacket(_session, packet);
    }
}​

 

Unity Project에 Player Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    public int PlayerId { get; set; }
}​

 

Unity Project에 MyPlayer Script 생성
using ServerCore;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MyPlayer : Player
{
    NetworkManager _network;

    void Start()
    {
        StartCoroutine("CoSendPacket");
        _network = GameObject.Find("NetworkManager").GetComponent<NetworkManager>();
    }

    void Update()
    {
        
    }

    IEnumerator CoSendPacket()
    {
        while (true)
        {
            yield return new WaitForSeconds(0.25f);

            C_Move movePacket = new C_Move();
            movePacket.posX = UnityEngine.Random.Range(-50, 50);
            movePacket.posY = 0;
            movePacket.posZ = UnityEngine.Random.Range(-50, 50);

            _network.Send(movePacket.Write());
        }
    }
}​

 

Unity Project에 PlayerManager Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerManager // 기생하는 것이 아닌 데이터만 들고 있도록 MonoBehaviour 상속 X
{
    MyPlayer _myPlayer;
    Dictionary<int, Player> _players = new Dictionary<int, Player>();

    public static PlayerManager Instance { get; } = new PlayerManager();

    public void Add(S_PlayerList packet)
    {
        Object obj = Resources.Load("Player");

        foreach (S_PlayerList.Player p in packet.players)
        {
            GameObject go = Object.Instantiate(obj) as GameObject;

            if (p.isSelf)
            {
                MyPlayer myPlayer = go.AddComponent<MyPlayer>();
                myPlayer.PlayerId = p.playerId;
                myPlayer.transform.position = new Vector3(p.posX, p.posY, p.posZ);
                _myPlayer = myPlayer;
            }
            else
            {
                Player player = go.AddComponent<Player>();
                player.PlayerId = p.playerId;
                player.transform.position = new Vector3(p.posX, p.posY, p.posZ);
                _players.Add(p.playerId, player);
            }
        }
    }

    public void Move(S_BroadcastMove packet)
    {
        if (_myPlayer.PlayerId == packet.playerId)
        {
            _myPlayer.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
        }
        else
        {
            Player player = null;
            if (_players.TryGetValue(packet.playerId, out player))
            {
                player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
            }
        }
    }

    public void EnterGame(S_BroadcastEnterGame packet)
    {
        if (packet.playerId == _myPlayer.PlayerId)
            return;

        Object obj = Resources.Load("Player");
        GameObject go = Object.Instantiate(obj) as GameObject;

        Player player = go.AddComponent<Player>();
        player.transform.position = new Vector3(packet.posX, packet.posY, packet.posZ);
        _players.Add(packet.playerId, player);
    }

    public void LeaveGame(S_BroadcastLeaveGame packet)
    {
        if (_myPlayer.PlayerId == packet.playerId)
        {
            GameObject.Destroy(_myPlayer.gameObject);
            _myPlayer = null;
        }
        else
        {
            Player player = null;
            if (_players.TryGetValue(packet.playerId, out player))
            {
                GameObject.Destroy(player.gameObject);
                _players.Remove(packet.playerId);
            }
        }
    }
}​

 

Unity Project의 PacketHandler Script 수정
using DummyClient;
using ServerCore;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

class PacketHandler
{
    public static void S_BroadcastEnterGameHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastEnterGame pkt = packet as S_BroadcastEnterGame;
        ServerSession serverSession = session as ServerSession;

        PlayerManager.Instance.EnterGame(pkt);
    }

    public static void S_BroadcastLeaveGameHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastLeaveGame pkt = packet as S_BroadcastLeaveGame;
        ServerSession serverSession = session as ServerSession;

        PlayerManager.Instance.LeaveGame(pkt);
    }

    public static void S_PlayerListHandler(PacketSession session, IPacket packet)
    {
        S_PlayerList pkt = packet as S_PlayerList;
        ServerSession serverSession = session as ServerSession;

        PlayerManager.Instance.Add(pkt);
    }

    public static void S_BroadcastMoveHandler(PacketSession session, IPacket packet)
    {
        S_BroadcastMove pkt = packet as S_BroadcastMove;
        ServerSession serverSession = session as ServerSession;

        PlayerManager.Instance.Move(pkt);
    }
}​

 

 

 

 

 

[ 섹션 2. Unity 2D ]

# 2D 환경 준비

- 프로젝트 생성 후 2D <~> 3D 간의 전환은 [ Edit ] - [ Project Settings... ] - [ Editor ] - [ Default Behaviour Mode ] 의

  [ Mode ] 에서 설정 가능하다.

- 3D에서 2D로 전환한 경우 [ Window ] - [ Package Manager ] 에서 "2D Tilemap Editor"를 직접 설치해준다.

 

# TileMap 기초

- Pixel Per Unit에서 Unit은 Unity의 거리 단위로, Grid 눈금 1칸이 1x1 Unit에 해당한다.

- Tile Palette는 [ Window ] - [ 2D ] - [ Tile Palette ] 에서 사용 가능하다.

 ~> Create New Palette 를 통해 새로운 Tile Palette 생성이 가능하다.

 ~> 사용하고자 하는 Tile, Sprite, Sprite Texture를 끌어 Tile Palette에 옮긴다.

 ~> [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 2D Object ] - [ Tilemap ] 을 통해 Tilemap 생성이 가능하다.

 ~> 생성한 Tilemap 위에 원하는 Tile을 페인트 칠한다.

 

# TileMap Layer

- Tilemap의 Tilemap Renderer Component 에서 Order in Layer 를 통해 Tilemap 간의 Render 우선순위 설정이 가능하다.

  (숫자가 낮은 Layer가 먼저 Rendering 되고, 숫자가 높은 Layer가 그 위로 Overlap 된다.)

 

# TileMap Collision

 

- Tilemap Collider 2D Component를 추가하는 것보다는 Collider를 위한 Tilemap을 추가하는 것이 Collider를 보다 더

 유연하게 만들 수 있다.

 

# MapTool

- Client 뿐만이 아니라 Server 역시 Map에 관한 정보를 가지고 있어야 한다. (예로 이동 가능/불가 지역과 같은 정보)

 ~> Server에게 보내기 위한 Map에 관한 정보가 담긴 파일을 Tool을 통해 쉽고 빠르게 생성할 수 있다.

- Tool은 Assets/Editor 경로 안에 위치한다.

 ~> Editor 폴더는 작성한 Script가 Unity Editor Scripting API에 접근할 수 있도록 허가해주는 예약 폴더이다.

 ~> 또 다른 예약 폴더로 Resources 폴더가 있으며, 이는 드래그 앤 드롭 대신 Script에서 파일 경로나 이름을 통해 Asset에

     접근할 수 있도록 만들어준다.

Assets/Editor 경로 안에 MapEditor Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using System.IO;

#if UNITY_EDITOR // 전처리기 ~> Unity Editor 상에서는 컴파일되지만, 그 외에는 컴파일되지 않는 코드
using UnityEditor;
#endif

public class MapEditor
{

#if UNITY_EDITOR // 전처리기 ~> Unity Editor 상에서는 컴파일되지만, 그 외에는 컴파일되지 않는 코드

    [MenuItem("Tools/GenerateMap %#g")] // 단축키 : %(Ctrl), #(Shift), &(Alt)
    private static void GenerateMap()
    {
        GameObject[] gameObjects = Resources.LoadAll<GameObject>("Prefabs/Map");

        foreach (GameObject go in gameObjects)
        {
            Tilemap tm = Util.FindChild<Tilemap>(go, "Tilemap_Collision", true);

            using (var writer = File.CreateText($"Assets/Resources/Map/{go.name}.txt"))
            {
                writer.WriteLine(tm.cellBounds.xMin);
                writer.WriteLine(tm.cellBounds.xMax);
                writer.WriteLine(tm.cellBounds.yMin);
                writer.WriteLine(tm.cellBounds.yMax);

                // Tilemap의 모든 position을 스캔하여 해당 position에 tile이 깔려 있는지 확인
                //  ~> 이동 가능 지역(즉, tile이 X)은 0, 이동 불가 지역(즉, tile이 O)은 1로
                for (int y = tm.cellBounds.yMax; y >= tm.cellBounds.yMin; y--)
                {
                    for (int x = tm.cellBounds.xMin; x <= tm.cellBounds.xMax; x++)
                    {
                        TileBase tile = tm.GetTile(new Vector3Int(x, y, 0));
                        if (tile != null)
                            writer.Write("1");
                        else
                            writer.Write("0");
                    }
                    writer.WriteLine();
                }
            }
        }
    }

#endif

}​

 

새로 추가된 Unity Custom Editor Menu
Tool을 통해 쉽고 빠르게 파일 생성

 

# Player 이동

- Player 이동을 Grid 눈금 1칸 단위로 움직이도록 구현하고자 한다.

Define Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum MoveDir
    {
        None,
        Up,
        Down,
        Left,
        Right,
    }

    // ...
}​

 

PlayerController Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : MonoBehaviour
{
    public Grid _grid;
    public float _speed = 5.0f;

    Vector3Int _cellPos = Vector3Int.zero;
    MoveDir _dir = MoveDir.None;
    bool _isMoving = false; // false인 경우 좌표 이동이 가능, true인 경우 좌표 이동이 불가능

    void Start()
    {
        // CellToWorld() 함수는 Grid를 기반으로 특정 Cell 위치의 World 좌표를 얻고 싶은 경우에 사용
        Vector3 pos = _grid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f);
        transform.position = pos;
    }

    void Update()
    {
        // Server 연동을 위한 분리 처리 (과부하 방지)
        GetDirInput();
        UpdatePosition();
        UpdateIsMoving();
    }

    // 키보드 입력을 통한 방향 설정
    void GetDirInput()
    {
        if (Input.GetKey(KeyCode.W))
        {
            _dir = MoveDir.Up;
        }
        else if (Input.GetKey(KeyCode.S))
        {
            _dir = MoveDir.Down;
        }
        else if (Input.GetKey(KeyCode.A))
        {
            _dir = MoveDir.Left;
        }
        else if (Input.GetKey(KeyCode.D))
        {
            _dir = MoveDir.Right;
        }
        else
        {
            _dir = MoveDir.None;
        }
    }

    // 자연스러운 이동 처리
    void UpdatePosition()
    {
        if (_isMoving == false)
            return;

        Vector3 destPos = _grid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f);
        Vector3 moveDir = destPos - transform.position; // 방향 벡터 (방향, 크기)
        
        // 도착 여부 체크
        float dist = moveDir.magnitude; // 방향 벡터의 크기 (즉, 목적지까지 남은 거리)
        if (dist < _speed * Time.deltaTime) // 거의 도착한 경우
        {
            transform.position = destPos;
            _isMoving = false;
        }
        else
        {
            transform.position += moveDir.normalized * _speed * Time.deltaTime;
            _isMoving = true;
        }
    }

    // 이동 가능한 상태인 경우 실제 좌표를 이동
    void UpdateIsMoving()
    {
        if (_isMoving == false)
        {
            switch (_dir)
            {
                case MoveDir.Up:
                    _cellPos += Vector3Int.up;
                    _isMoving = true;
                    break;
                case MoveDir.Down:
                    _cellPos += Vector3Int.down;
                    _isMoving = true;
                    break;
                case MoveDir.Left:
                    _cellPos += Vector3Int.left;
                    _isMoving = true;
                    break;
                case MoveDir.Right:
                    _cellPos += Vector3Int.right;
                    _isMoving = true;
                    break;
            }
        }
    }
}​

 

# Player Animation

PlayerController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : MonoBehaviour
{
    // ...

    MoveDir _dir = MoveDir.None;
    Animator _animator;
    public MoveDir Dir
    {
        get { return _dir; }
        set
        {
            if (_dir == value)
                return;

            switch (value)
            {
                case MoveDir.Up:
                    _animator.Play("WALK_BACK");
                    transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
                    break;
                case MoveDir.Down:
                    _animator.Play("WALK_FRONT");
                    transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
                    break;
                case MoveDir.Left:
                    _animator.Play("WALK_RIGHT");
                    transform.localScale = new Vector3(-1.0f, 1.0f, 1.0f);
                    break;
                case MoveDir.Right:
                    _animator.Play("WALK_RIGHT");
                    transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
                    break;
                case MoveDir.None:
                    if (_dir == MoveDir.Up)
                    {
                        _animator.Play("IDLE_BACK");
                        transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
                    }
                    else if (_dir == MoveDir.Down)
                    {
                        _animator.Play("IDLE_FRONT");
                        transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
                    }
                    else if (_dir == MoveDir.Left)
                    {
                        _animator.Play("IDLE_RIGHT");
                        transform.localScale = new Vector3(-1.0f, 1.0f, 1.0f);
                    }
                    else
                    {
                        _animator.Play("IDLE_RIGHT");
                        transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
                    }
                    break;
            }

            _dir = value;
        }
    }

    void Start()
    {
        // ...
        
        _animator = GetComponent<Animator>();
    }

    // ...

    void GetDirInput()
    {
        if (Input.GetKey(KeyCode.W))
        {
            Dir = MoveDir.Up;
        }
        else if (Input.GetKey(KeyCode.S))
        {
            Dir = MoveDir.Down;
        }
        else if (Input.GetKey(KeyCode.A))
        {
            Dir = MoveDir.Left;
        }
        else if (Input.GetKey(KeyCode.D))
        {
            Dir = MoveDir.Right;
        }
        else
        {
            Dir = MoveDir.None;
        }
    }

    // ...
}​

 

# MapManager

MapManager Script 생성
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

public class MapManager
{
    public Grid CurrentGrid { get; private set; }

    public int MinX { get; set; }
    public int MaxX { get; set; }
    public int MinY { get; set; }
    public int MaxY { get; set; }

    bool[,] _collision;

    public bool CanGo(Vector3Int cellPos)
    {
        if (cellPos.x < MinX || cellPos.x > MaxX)
            return false;
        if (cellPos.y < MinY || cellPos.y > MaxY) 
            return false;

        int x = cellPos.x - MinX;
        int y = MaxY - cellPos.y;
        return !_collision[y, x];
    }

    public void LoadMap(int mapId)
    {
        DestroyMap(); // 오류 방지 차원

        // 맵 로드
        string mapName = "Map_" + mapId.ToString("000");
        GameObject go = Managers.Resource.Instantiate($"Map/{mapName}");
        go.name = "Map";

        GameObject collision = Util.FindChild(go, "Tilemap_Collision", true);
        if (collision != null)
            collision.SetActive(false);

        CurrentGrid = go.GetComponent<Grid>();

        // Collision txt 파일을 통해 이동 가능 여부에 관한 데이터 저장
        TextAsset txt = Managers.Resource.Load<TextAsset>($"Map/{mapName}");
        StringReader reader = new StringReader(txt.text);

        MinX = int.Parse(reader.ReadLine());
        MaxX = int.Parse(reader.ReadLine());
        MinY = int.Parse(reader.ReadLine());
        MaxY = int.Parse(reader.ReadLine());

        int xCount = MaxX - MinX + 1;
        int yCount = MaxY - MinY + 1;
        _collision = new bool[yCount, xCount];
        
        for (int y = 0; y < yCount; y++)
        {
            string line = reader.ReadLine();
            for (int x = 0; x < xCount; x++)
            {
                _collision[y, x] = (line[x] == '1' ? true : false);
            }
        }
    }

    public void DestroyMap()
    {
        GameObject map = GameObject.Find("Map");
        if (map != null)
        {
            GameObject.Destroy(map);
            CurrentGrid = null;
        }
    }
}​

 

Managers Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    // ...

    #region Contents
    MapManager _map = new MapManager();

    public static MapManager Map { get { return Instance._map; } }
    #endregion

    // ...
}​

 

GameScene Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameScene : BaseScene
{
    protected override void Init()
    {
        // ...

        Managers.Map.LoadMap(1);
        
        // ...
    }
}​

 

MapEditor Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
using System.IO;

#if UNITY_EDITOR
using UnityEditor;
#endif

public class MapEditor
{

#if UNITY_EDITOR

    [MenuItem("Tools/GenerateMap %#g")]
    private static void GenerateMap()
    {
        // ...

        foreach (GameObject go in gameObjects)
        {
            // 전체적인 Map 은 Tilemap_Collision 이 아닌 Tilemap_Base 에 해당하므로 수정
            Tilemap tmBase = Util.FindChild<Tilemap>(go, "Tilemap_Base", true);
            Tilemap tm = Util.FindChild<Tilemap>(go, "Tilemap_Collision", true);

            using (var writer = File.CreateText($"Assets/Resources/Map/{go.name}.txt"))
            {
                writer.WriteLine(tmBase.cellBounds.xMin); // tm에서 tmBase로 수정
                writer.WriteLine(tmBase.cellBounds.xMax); // tm에서 tmBase로 수정
                writer.WriteLine(tmBase.cellBounds.yMin); // tm에서 tmBase로 수정
                writer.WriteLine(tmBase.cellBounds.yMax); // tm에서 tmBase로 수정

                for (int y = tmBase.cellBounds.yMax; y >= tmBase.cellBounds.yMin; y--) // tm에서 tmBase로 수정
                {
                    for (int x = tmBase.cellBounds.xMin; x <= tmBase.cellBounds.xMax; x++) // tm에서 tmBase로 수정
                    {
                        // ...
                    }
                    
                    // ...
                }
            }
        }
    }

#endif

}​

 

PlayerController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : MonoBehaviour
{
    // public Grid _grid; ~> 더이상 드래그 앤 드롭으로 연결 X
    
    // ...

    void Start()
    {
        Vector3 pos = Managers.Map.CurrentGrid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f); // Managers.Map.CurrentGrid 을 통해 현재 Grid를 참조
        
        // ...
    }

    // ...

    void UpdatePosition()
    {
        // ...

        Vector3 destPos = Managers.Map.CurrentGrid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f); // Managers.Map.CurrentGrid 을 통해 현재 Grid를 참조
        
        // ...
    }

    void UpdateIsMoving()
    {
        if (_isMoving == false && _dir != MoveDir.None)
        {
            Vector3Int destPos = _cellPos;

            switch (_dir)
            {
                case MoveDir.Up:
                    destPos += Vector3Int.up;
                    break;
                case MoveDir.Down:
                    destPos += Vector3Int.down;
                    break;
                case MoveDir.Left:
                    destPos += Vector3Int.left;
                    break;
                case MoveDir.Right:
                    destPos += Vector3Int.right;
                    break;
            }

            if (Managers.Map.CanGo(destPos))
            {
                _cellPos = destPos;
                _isMoving = true;
            }
        }
    }
}​

 

# Controller 정리

- _isMoving, _isSkill, _isJumping과 같이 bool 타입의 변수를 계속해서 생성하는 것보단 state에 관한 enum을 생성하고

  이를 통해 상태를 관리하는 것이 더 좋다.

Define Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum CreatureState
    {
        Idle,
        Moving,
        Skill,
        Dead,
    }

    // ...
}​

 

CreatureController Script 생성
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class CreatureController : MonoBehaviour
{
    public float _speed = 5.0f;
    protected Vector3Int _cellPos = Vector3Int.zero;
    protected Animator _animator;
    protected SpriteRenderer _sprite; // flipX 사용을 위해
    // protected bool _isMoving = false;

    CreatureState _state = CreatureState.Idle;
    public CreatureState State
    {
        get { return _state; }
        set
        {
            if (_state == value)
                return;

            _state = value;
            UpdateAnimation();
        }
    }

    MoveDir _dir = MoveDir.None;
    MoveDir _lastDir = MoveDir.None; // 마지막으로 바라보고 있던 방향
    public MoveDir Dir
    {
        get { return _dir; }
        set
        {
            if (_dir == value)
                return;

            _dir = value;
            if (value != MoveDir.None) // 마지막으로 바라보고 있던 방향 설정
                _lastDir = value;

            UpdateAnimation();
        }
    }

    // 상태 전환, 방향 전환에 따른 Animation 변경을 위한 함수 생성
    protected virtual void UpdateAnimation()
    {
        if (_state == CreatureState.Idle)
        {
            switch (_lastDir)
            {
                case MoveDir.Up:
                    _animator.Play("IDLE_BACK");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Down:
                    _animator.Play("IDLE_FRONT");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Left:
                    _animator.Play("IDLE_RIGHT");
                    _sprite.flipX = true;
                    break;
                case MoveDir.Right:
                    _animator.Play("IDLE_RIGHT");
                    _sprite.flipX = false;
                    break;
            }
        }
        else if (_state == CreatureState.Moving)
        {
            switch (_dir)
            {
                case MoveDir.Up:
                    _animator.Play("WALK_BACK");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Down:
                    _animator.Play("WALK_FRONT");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Left:
                    _animator.Play("WALK_RIGHT");
                    _sprite.flipX = true;
                    break;
                case MoveDir.Right:
                    _animator.Play("WALK_RIGHT");
                    _sprite.flipX = false;
                    break;
            }
        }
        else if (_state == CreatureState.Skill)
        {

        }
        else
        {

        }
    }

    void Start()
    {
        Init();
    }

    void Update()
    {
        UpdateController();
    }

    protected virtual void Init()
    {
        _animator = GetComponent<Animator>();
        _sprite = GetComponent<SpriteRenderer>();
        Vector3 pos = Managers.Map.CurrentGrid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f); // Managers.Map.CurrentGrid 을 통해 현재 Grid를 참조
        transform.position = pos;
    }

    protected virtual void UpdateController()
    {
        UpdatePosition();
        UpdateIsMoving();
    }

    // 자연스러운 이동 처리
    void UpdatePosition()
    {
        if (State != CreatureState.Moving)
            return;

        Vector3 destPos = Managers.Map.CurrentGrid.CellToWorld(_cellPos) + new Vector3(0.5f, 0.5f); // Managers.Map.CurrentGrid 을 통해 현재 Grid를 참조
        Vector3 moveDir = destPos - transform.position; // 방향 벡터 (방향, 크기)

        // 도착 여부 체크
        float dist = moveDir.magnitude; // 방향 벡터의 크기 (즉, 목적지까지 남은 거리)
        if (dist < _speed * Time.deltaTime) // 거의 도착한 경우
        {
            transform.position = destPos;
            // 예외적으로 Animation을 직접 컨트롤
            _state = CreatureState.Idle;
            if (_dir == MoveDir.None)
                UpdateAnimation();
        }
        else
        {
            transform.position += moveDir.normalized * _speed * Time.deltaTime;
            State = CreatureState.Moving;
        }
    }

    // 이동 가능한 상태인 경우 실제 좌표를 이동
    void UpdateIsMoving()
    {
        if (State == CreatureState.Idle && _dir != MoveDir.None)
        {
            Vector3Int destPos = _cellPos;

            switch (_dir)
            {
                case MoveDir.Up:
                    destPos += Vector3Int.up;
                    break;
                case MoveDir.Down:
                    destPos += Vector3Int.down;
                    break;
                case MoveDir.Left:
                    destPos += Vector3Int.left;
                    break;
                case MoveDir.Right:
                    destPos += Vector3Int.right;
                    break;
            }

            if (Managers.Map.CanGo(destPos))
            {
                _cellPos = destPos;
                State = CreatureState.Moving;
            }
        }
    }
}​

 

PlayerController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : CreatureController
{
    protected override void Init()
    {
        base.Init();
    }

    protected override void UpdateController()
    {
        GetDirInput();
        base.UpdateController();
    }

    void LateUpdate()
    {
        Camera.main.transform.position = new Vector3(transform.position.x, transform.position.y, -10);
    }

    // 키보드 입력을 통한 방향 설정
    void GetDirInput()
    {
        if (Input.GetKey(KeyCode.W))
        {
            Dir = MoveDir.Up;
        }
        else if (Input.GetKey(KeyCode.S))
        {
            Dir = MoveDir.Down;
        }
        else if (Input.GetKey(KeyCode.A))
        {
            Dir = MoveDir.Left;
        }
        else if (Input.GetKey(KeyCode.D))
        {
            Dir = MoveDir.Right;
        }
        else
        {
            Dir = MoveDir.None;
        }
    }
}​

 

MonsterController Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class MonsterController : CreatureController
{
    protected override void Init()
    {
        base.Init();
    }

    protected override void UpdateController()
    {
        //GetDirInput();
        base.UpdateController();
    }

    // 키보드 입력을 통한 방향 설정
    void GetDirInput()
    {
        if (Input.GetKey(KeyCode.W))
        {
            Dir = MoveDir.Up;
        }
        else if (Input.GetKey(KeyCode.S))
        {
            Dir = MoveDir.Down;
        }
        else if (Input.GetKey(KeyCode.A))
        {
            Dir = MoveDir.Left;
        }
        else if (Input.GetKey(KeyCode.D))
        {
            Dir = MoveDir.Right;
        }
        else
        {
            Dir = MoveDir.None;
        }
    }
}​

 

# ObjectManager

- Player끼리, Monster끼리, Player와 Monster간의 충돌이 발생할 수 있도록 만들어볼 예정이다.

ObjectManager Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectManager
{
    List<GameObject> _objects = new List<GameObject>();

    public void Add(GameObject go)
    {
        _objects.Add(go);
    }

    public void Remove(GameObject go)
    {
        _objects.Remove(go);
    }

    public GameObject Find(Vector3Int cellPos)
    {
        foreach (GameObject obj in _objects)
        {
            CreatureController cc = obj.GetComponent<CreatureController>();
            if (cc == null)
                continue;

            if (cc.CellPos == cellPos)
                return obj;
        }

        return null;
    }

    public void Clear()
    {
        _objects.Clear();
    }
}​

 

Managers Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    // ...

    #region Contents
    // ...
    
    ObjectManager _obj = new ObjectManager();
    
    public static ObjectManager Object { get { return Instance._obj;} }
    
    // ...
    #endregion

    // ...
}​

 

CreatureController Script 수정
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class CreatureController : MonoBehaviour
{
    // ...

    // 외부에서 접근할 수 있도록 수정
    public Vector3Int CellPos { get; set; } = Vector3Int.zero;

    // ...

    protected virtual void Init()
    {
        // ...
        
        Vector3 pos = Managers.Map.CurrentGrid.CellToWorld(CellPos) + new Vector3(0.5f, 0.5f); // _cellPos를 CellPos로 수정
        
        // ...
    }

    // ...

    void UpdatePosition()
    {
        // ...

        Vector3 destPos = Managers.Map.CurrentGrid.CellToWorld(CellPos) + new Vector3(0.5f, 0.5f); // _cellPos를 CellPos로 수정
        
        // ...
    }

    void UpdateIsMoving()
    {
        if (State == CreatureState.Idle && _dir != MoveDir.None)
        {
            Vector3Int destPos = CellPos; // _cellPos를 CellPos로 수정

            // ...

            State = CreatureState.Moving;
            if (Managers.Map.CanGo(destPos))
            {
                if (Managers.Object.Find(destPos) == null) // 만약 destPos 위치에 Object가 존재하지 않는 경우
                {
                    CellPos = destPos; // _cellPos를 CellPos로 수정
                }     
            }
        }
    }
}​

 

GameScene Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameScene : BaseScene
{
    protected override void Init()
    {
        // ...

        // Player와 Monster를 Prefab화 시킨뒤 수동으로 생성
        GameObject player = Managers.Resource.Instantiate("Creature/Player");
        player.name = "Player";
        Managers.Object.Add(player); // 생성한 Player를 ObjectManager의 _objects List에 추가

        for (int i = 0; i < 5; i++)
        {
            GameObject monster = Managers.Resource.Instantiate("Creature/Monster");
            monster.name = $"Monster_{i + 1}";

            // 랜덤 위치 스폰 (일단 겹쳐도 OK)
            Vector3Int pos = new Vector3Int()
            {
                x = Random.Range(-10, 10),
                y = Random.Range(-5, 5),
            };
            MonsterController mc = monster.GetComponent<MonsterController>();
            mc.CellPos = pos;

            Managers.Object.Add(monster); // 생성한 Monster를 ObjectManager의 _objects List에 추가
        }

        // ...
    }

    // ...
}​

 

# 평타 스킬

CreatureController Script 수정
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class CreatureController : MonoBehaviour
{
    // ...

    public Vector3Int GetFrontCellPos() // 현재 바라보고 있는 방향의 Cell위치를 얻기 위한 함수
    {
        Vector3Int cellPos = CellPos;

        switch (_lastDir)
        {
            case MoveDir.Up:
                cellPos += Vector3Int.up;
                break;
            case MoveDir.Down:
                cellPos += Vector3Int.down;
                break;
            case MoveDir.Left:
                cellPos += Vector3Int.left;
                break;
            case MoveDir.Right:
                cellPos += Vector3Int.right;
                break;
        }
        return cellPos;
    }

    protected virtual void UpdateAnimation()
    {
        // ...
        else if (_state == CreatureState.Skill)
        {
            switch (_lastDir)
            {
                case MoveDir.Up:
                    _animator.Play("ATTACK_BACK");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Down:
                    _animator.Play("ATTACK_FRONT");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Left:
                    _animator.Play("ATTACK_RIGHT");
                    _sprite.flipX = true;
                    break;
                case MoveDir.Right:
                    _animator.Play("ATTACK_RIGHT");
                    _sprite.flipX = false;
                    break;
            }
        }
        else
        {

        }
    }

    // ...

    protected virtual void UpdateController()
    {
        switch (State)
        {
            case CreatureState.Idle:
                UpdateIdle();
                break;
            case CreatureState.Moving:
                UpdateMoving();
                break;
            case CreatureState.Skill:
                break;
            case CreatureState.Dead:
                break;
        }
    }

    // 자연스러운 이동 처리
    protected virtual void UpdateMoving() // UpdatePosition() 에서 UpdateMoving()으로 이름 변경
    {
        // UpdateController()의 switch문에서 확인하기 때문에 아래 부분 삭제
        // if (State != CreatureState.Moving)
        //     return;

        Vector3 destPos = Managers.Map.CurrentGrid.CellToWorld(CellPos) + new Vector3(0.5f, 0.5f);
        Vector3 moveDir = destPos - transform.position;

        float dist = moveDir.magnitude;
        if (dist < _speed * Time.deltaTime)
        {
            transform.position = destPos;

            _state = CreatureState.Idle;
            if (_dir == MoveDir.None)
                UpdateAnimation();
        }
        else
        {
            transform.position += moveDir.normalized * _speed * Time.deltaTime;
            State = CreatureState.Moving;
        }
    }

    // 이동 가능한 상태인 경우 실제 좌표를 이동
    protected virtual void UpdateIdle() // UpdateIsMoving() 에서 UpdateIdle()로 이름 변경
    {
        if (_dir != MoveDir.None) // UpdateController()의 switch문에서 확인하기 때문에 "State == CreatureState.Idle &&" 부분 삭제
        {
            Vector3Int destPos = CellPos;

            switch (_dir)
            {
                case MoveDir.Up:
                    destPos += Vector3Int.up;
                    break;
                case MoveDir.Down:
                    destPos += Vector3Int.down;
                    break;
                case MoveDir.Left:
                    destPos += Vector3Int.left;
                    break;
                case MoveDir.Right:
                    destPos += Vector3Int.right;
                    break;
            }

            State = CreatureState.Moving;
            if (Managers.Map.CanGo(destPos))
            {
                if (Managers.Object.Find(destPos) == null)
                {
                    CellPos = destPos;
                }     
            }
        }
    }

    protected virtual void UpdateSkill()
    {

    }

    protected virtual void UpdateDead()
    {

    }
}​

 

PlayerController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : CreatureController
{
    Coroutine _coSkill;

    // ...

    protected override void UpdateController()
    {
        switch (State) 
        {
            case CreatureState.Idle:
                GetDirInput();
                GetIdleInput(); // 움직이지 않는 경우에만 Skill 사용이 가능하도록
                break;
            case CreatureState.Moving:
                GetDirInput();
                break;
        }
        base.UpdateController();
    }

    // ...

    void GetIdleInput()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            State = CreatureState.Skill;
            _coSkill = StartCoroutine("CoStartPunch");
        }    
    }

    IEnumerator CoStartPunch() // 스킬 쿨타임
    {
        // 피격 판정
        GameObject go = Managers.Object.Find(GetFrontCellPos());
        if (go != null)
        {
            Debug.Log(go.name);
        }

        // 대기 시간
        yield return new WaitForSeconds(0.5f);
        State = CreatureState.Idle;
        _coSkill = null;
    }
}​

 

# 화살 스킬

CreatureController Script 수정
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class CreatureController : MonoBehaviour
{
    // ...

    protected CreatureState _state = CreatureState.Idle; // 보호수준을 protected로 변경

    // ...
    
    protected MoveDir _dir = MoveDir.None; // 보호수준을 protected로 변경
    protected MoveDir _lastDir = MoveDir.None; // 보호수준을 protected로 변경
    
    // ...
}​

 

ArrowController Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class ArrowController : CreatureController
{
    protected override void Init()
    {
        switch (_lastDir) // 화살이 나아가야하는 방향에 맞춰 회전
        {
            case MoveDir.Up:
                transform.rotation = Quaternion.Euler(0, 0, 0);
                break;
            case MoveDir.Down:
                transform.rotation = Quaternion.Euler(0, 0, -180);
                break;
            case MoveDir.Left:
                transform.rotation = Quaternion.Euler(0, 0, -90);
                break;
            case MoveDir.Right:
                transform.rotation = Quaternion.Euler(0, 0, 90);
                break;
        }
        base.Init();
    }

    protected override void UpdateAnimation()
    {
        // 화살은 Animation이 필요 X ~> override 후 내용을 공백으로 둔다.
    }

    protected override void UpdateIdle() // 화살의 자연스러운 이동 처리
    {
        if (_dir != MoveDir.None)
        {
            Vector3Int destPos = CellPos;

            switch (_dir)
            {
                case MoveDir.Up:
                    destPos += Vector3Int.up;
                    break;
                case MoveDir.Down:
                    destPos += Vector3Int.down;
                    break;
                case MoveDir.Left:
                    destPos += Vector3Int.left;
                    break;
                case MoveDir.Right:
                    destPos += Vector3Int.right;
                    break;
            }

            State = CreatureState.Moving;
            if (Managers.Map.CanGo(destPos))
            {
                GameObject go = Managers.Object.Find(destPos);
                if (go == null)
                {
                    CellPos = destPos;
                }
                else // 만약 화살이 나아가는 방향에 Object가 존재할 경우
                {
                    Debug.Log(go.name);
                    Managers.Resource.Destroy(gameObject); // 화살 소멸
                }
            }
            else // 만약 화살이 나아가는 방향이 이동 불가능한 지역일 경우
            {
                Managers.Resource.Destroy(gameObject); // 화살 소멸
            }
        }
    }
}​

 

PlayerController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : CreatureController
{
    // ...
    
    bool _rangeSkill = false; // Skill 사용시 해당 Skill이 범위 Skill 인지 구분하기 위해서

    // ...

    protected override void UpdateAnimation() // Player만 화살 Skill 사용이 가능하므로 override 하여 기능 추가
    {
        if (_state == CreatureState.Idle)
        {
            switch (_lastDir)
            {
                case MoveDir.Up:
                    _animator.Play("IDLE_BACK");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Down:
                    _animator.Play("IDLE_FRONT");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Left:
                    _animator.Play("IDLE_RIGHT");
                    _sprite.flipX = true;
                    break;
                case MoveDir.Right:
                    _animator.Play("IDLE_RIGHT");
                    _sprite.flipX = false;
                    break;
            }
        }
        else if (_state == CreatureState.Moving)
        {
            switch (_dir)
            {
                case MoveDir.Up:
                    _animator.Play("WALK_BACK");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Down:
                    _animator.Play("WALK_FRONT");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Left:
                    _animator.Play("WALK_RIGHT");
                    _sprite.flipX = true;
                    break;
                case MoveDir.Right:
                    _animator.Play("WALK_RIGHT");
                    _sprite.flipX = false;
                    break;
            }
        }
        else if (_state == CreatureState.Skill) // _rangeSkill 여부에 맞춰 Animation 재생
        {
            switch (_lastDir)
            {
                case MoveDir.Up:
                    _animator.Play(_rangeSkill ? "ATTACK_WEAPON_BACK": "ATTACK_BACK");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Down:
                    _animator.Play(_rangeSkill ? "ATTACK_WEAPON_FRONT" : "ATTACK_FRONT");
                    _sprite.flipX = false;
                    break;
                case MoveDir.Left:
                    _animator.Play(_rangeSkill ? "ATTACK_WEAPON_RIGHT" : "ATTACK_RIGHT");
                    _sprite.flipX = true;
                    break;
                case MoveDir.Right:
                    _animator.Play(_rangeSkill ? "ATTACK_WEAPON_RIGHT" : "ATTACK_RIGHT");
                    _sprite.flipX = false;
                    break;
            }
        }
        else
        {

        }
    }

    // ...

    void GetIdleInput()
    {
        if (Input.GetKey(KeyCode.Space))
        {
            State = CreatureState.Skill;
            _coSkill = StartCoroutine("CoStartShootArrow"); // Test를 위해 CoStartPunch가 아닌 CoStartShootArrow 실행
        }    
    }

    // ...

    IEnumerator CoStartShootArrow()
    {
        GameObject go = Managers.Resource.Instantiate("Creature/Arrow"); // 화살 생성
        ArrowController ac = go.GetComponent<ArrowController>();
        ac.Dir = _lastDir; // 화살 방향은 Player가 마지막으로 바라보고 있던 방향으로 설정
        ac.CellPos = CellPos; // 화살 발사 위치는 Player의 위치로 설정

        // 대기 시간
        _rangeSkill = true;
        yield return new WaitForSeconds(0.3f);
        State = CreatureState.Idle;
        _coSkill = null;
    }
}​

 

# 소멸 이펙트

- Player 및 Monster의 죽음은 다양한 원인에 의해 발생할 수 있다.

 ~> 죽음에 관한 Logic을 매번 복붙할 경우 유지보수가 힘들어질 수 있다. (즉, 한 곳에서 관리하는 것이 중요)

- 피격을 한 객체에서가 아닌 피격을 당한 객체에서 죽음에 관한 Logic을 처리하는 것이 더 좋다.

CreatureController Script 수정
using Newtonsoft.Json.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class CreatureController : MonoBehaviour
{
    // ...

    public virtual void OnDamaged()
    {

    }
}​

 

MonsterController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class MonsterController : CreatureController
{
    // ...

    public override void OnDamaged()
    {
        GameObject effect = Managers.Resource.Instantiate("Effect/DieEffect"); // Effect 생성
        effect.transform.position = transform.position; // Effect 위치를 화살 피격을 당한 Object 위치로 설정
        effect.GetComponent<Animator>().Play("START"); // Effect의 Animation 재생
        GameObject.Destroy(effect, 0.5f); // 0.5초 후 Effect 삭제

        Managers.Object.Remove(gameObject); // 화살 피격을 당한 Object를 _objects List 에서 삭제
        Managers.Resource.Destroy(gameObject); // 화살 피격을 당한 Object 삭제
    }
}​

 

PlayerController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class PlayerController : CreatureController
{
    // ...

    IEnumerator CoStartPunch()
    {
        GameObject go = Managers.Object.Find(GetFrontCellPos());
        if (go != null)
        {
            CreatureController cc = go.GetComponent<CreatureController>();
            if (cc != null)
                cc.OnDamaged(); // OnDamaged() 호출
        }

        _rangeSkill = false;
        yield return new WaitForSeconds(0.5f);
        State = CreatureState.Idle;
        _coSkill = null;
    }

    // ...
}​

 

ArrowController Script 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static Define;

public class ArrowController : CreatureController
{
    // ...

    protected override void UpdateIdle() 
    {
        if (_dir != MoveDir.None)
        {
            // ...

            if (Managers.Map.CanGo(destPos))
            {
                GameObject go = Managers.Object.Find(destPos);
                if (go == null)
                {
                    CellPos = destPos;
                }
                else
                {
                    CreatureController cc = go.GetComponent<CreatureController>();
                    if (cc != null)
                        cc.OnDamaged(); // OnDamaged() 호출

                    Managers.Resource.Destroy(gameObject);
                }
            }
            else 
            {
                // ...
            }
        }
    }
}​

 

# Patrol AI

- 집중이 안되서 듣다 말았음;; 다시 듣기..! 

 ~> CreatureController랑 PlayerController 수정함

 

# Search AI

-

 

# Skill AI

-

 

 

 

 

[ 섹션 0. 개론 ]

# 서버 OT

- 서버란 다른 컴퓨터에서 연결이 가능하도록 대기 상태로 상시 실행중인 프로그램이다.

- Web 서버 (aka. HTTP Server)

 ~> 테이크아웃 포장 전문 식당

 ~> 손님이 음식을 받아서 떠나면, 그 이후론 연락이 끊긴다.

 ~> 드물게 정보를 요청/갱신한다.

 ~> 식당에서 손님한테 먼저 접근할 일은 없다. (ex : 물 따라드릴까요?)

 ~> 주문 후 손님이 바로 떠나면, 손님의 상태를 당분간 잊고 지낸다. (Stateless)

 ~> Web 서버 제작은 처음부터 만드는 경우는 사실상 없고, Framework를 하나 골라서 사용한다.

 ~> 질의/응답 형태

- Game 서버 (aka. TCP Server, Binary Server, Stateful Server ...)

 ~> 일반 식당

 ~> 서빙 직원이 와서 손님에게 물어볼 수도 있고, 손님이 추가 주문을 하기도 한다.

 ~> 요청/갱신 횟수가 많다.

 ~> 언제라도 직원이 손님한테 접근이 가능해야 한다.

 ~> 손님이 식당에 머무는 동안, 손님의 상태를 보며 최상의 서비스를 제공한다. (Stateful)

 ~> 게임/장르에 따라 요구사항이 너무 달라 최적의 Framework라는 것이 존재하기 애매하다.

 ~> 실시간 Interaction이 존재

 

# 환경설정

- [ 솔루션 ] - [ 오른쪽 마우스 ] - [ 속성 ] 의 [ 공용 속성 ] - [ 시작 프로젝트 ] 에서 [ 한 개의 시작 프로젝트 ] 가 아닌

  [ 여러 개의 시작 프로젝트 ] 를 선택한 뒤 동시에 실행하고자하는 프로젝트의 작업 상태를 [ 없음 ] 에서 [ 시작 ] 으로

  변경하면 작업 상태가 [ 시작 ] 으로 설정된 여러개의 프로젝트를 동시에 실행 가능하다.

 

[ 섹션 1. 멀티쓰레드 프로그래밍 ]

# 멀티쓰레드 개론

- Process는 운영체제로부터 자원을 할당받는 작업의 단위이다.

- Thread는 한 Process가 할당받은 자원을 이용하는 실행의 단위이다.

- MultiThread란 하나의 Process를 다수의 실행 단위로 구분하여, 자원을 공유하고 자원의 생성과 관리의 중복성을

  최소화하여 수행 능력을 향상 시키는 것이다.

 

- 한 고급 레스토랑이 운영되기 위해서는 로봇 직원들이 필요하며 로봇 직원 각각에게는 업무가 주어진다. (계산, 서빙 ...)

 ~> 또한, 영혼의 개수가 한정적이기 때문에 동시에 작동시킬 수 있는 로봇 직원의 수 역시 제한된다.

- 위의 내용에서 고급 레스토랑은 Process, 로봇 직원은 Thread, 영혼은 CPU 코어에 해당한다.

 

- 영혼의 개수가 1개로 한정적이기 때문에 1명의 로봇 직원만 작동시킬 수 있다.

 ~> 만약, 해당 영혼을 4명의 직원에게 0.1초씩 빠르게 번갈아가면서 주입할 경우 어떻게 될까?

 ~> 우리의 눈에는 4명의 직원 모두 작동하는 것처럼 보일 것이다.

 

- 우리가 보기에는 다수의 Process가 동시에 동작되는 것처럼 보이지만, 실제로 컴퓨터는 운영체제의 Scheduling에 의해

  Task를 아주 짧은 시간동안 번갈아가며 수행하는 것이다.

 ~> 이를 MultiTasking 이라고 한다.

 

- CPU 코어의 Clock Cycle 증가를 통해 컴퓨터의 성능 개선을 꿈꿨던 개발자들은 전력 문제와 발열 문제라는 한계에 도달

  하게 되고, 이에 선택한 대안이 CPU 코어의 개수를 늘리는 것이다.

 ~> 이를 MultiProcessor 라고 한다.

- CPU 코어가 4개일 경우 동시에 실행할 수 있는 Thread는 4개이므로 CPU 코어의 수가 많다고 해서 Thread의 수를 늘려

  봤자 한계가 존재한다. ( CPU 코어의 수와 Thread의 수를 최대한 맞춰주는 것이 중요 )

 ~> 또한, Thread의 수가 많으면 많을수록 MultiTasking 부하가 상당해진다.

 

- Thread 사용시 Thread 배치는 구성하기 나름이다.

 

- MultiProcess 는 데이터 영역, Stack 영역, Heap 영역을 Process 끼리 전부 공유하지 않는다.

- MultiThread 는 Stack 영역을 제외한 데이터 영역, Heap 영역은 Thread 끼리 전부 공유한다.

 

+ 추가 검색 (https://inpa.tistory.com/entry/%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB-multi-process-multi-thread)

- MultiProcessor 와 MultiProcess 의 차이점

 ~> Processor는 CPU 코어를, Process는 프로그램의 실행 상태를 일컫는다.

 ~> MultiProcessor 는 여러개의 CPU 코어가 하나의 시스템에서 동시에 실행되는 것을 의미한다.

 ~> MultiProcess 는 하나의 프로그램에서 여러 개의 Process를 실행하는 것을 의미한다. ( ex : fork() )

 

+ 추가 검색 ( https://wooody92.github.io/os/%EB%A9%80%ED%8B%B0-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4%EC%99%80-%EB%A9%80%ED%8B%B0-%EC%8A%A4%EB%A0%88%EB%93%9C/ )

 - MultiProcess 와 MultiThread 의 차이점

 1. MultiThread는 MultiProcess 보다 적은 메모리 공간을 차지하고, Context Switching 이 빠르다는 장점을 가진다.

  ~> 그러나 MultiThread의 단점으로는 동기화 문제와 1개의 Thread 장애가 전체 Thread 에게 영향을 줄 수 있다는 것이다.

 2. MultiProcess는 1개의 Process가 죽더라도 다른 Process 에게 영향을 주지 않아 안정성이 높다는 장점을 가진다.

  ~> 그러나 MultiProcess의 단점으로는 MultiThread 보다 많은 메모리 공간과 CPU 시간을 차지한다는 것이다.

 

# 쓰레드 생성

- 고급 레스토랑에서 정직원을 고용하는 것은 상당히 부담스럽다. ( 인건비, 4대 보험 등 ... )

 ~> 인력 상담소에서 단기 알바를 구하는 것이 좋다.

- 즉, Thread를 생성하는 것은 상당한 부하를 가져다 준다.

 ~> C#은 사용 가능한 Thread를 할당 받아 사용할 수 있는 Thread Pool을 지원한다. ( 사용 후 반환 / 일꾼이 없으면 대기 )

Thread 응용
using System;
using System.Threading; // 추가해야 Thread 사용 가능

namespace ServerCore
{
    class Program
    {
        static void MainThread()
        {
            for (int i = 0; i < 5; i++)
                Console.WriteLine("Hello Thread!");
        }

        static void Main(string[] args)
        {
            Thread t = new Thread(MainThread); // 한명의 직원을 고용한 뒤 MainThread 라는 업무를 할당한 것
            t.IsBackground = true; // Main 함수가 종료될 경우 해당 Thread도 종료 (기본값은 foreground)
            t.Name = "Test Thread"; // Thread 이름 설정
            t.Start(); // Thread 실행

            Console.WriteLine("Waiting for Thread");
            t.Join(); // 해당 Thread가 종료될때까지 기다리는 것

            Console.WriteLine("Hello World");
        }
    }
}​

 

Thread Pool 응용
using System;
using System.Threading;

namespace ServerCore
{
    class Program
    {
        static void MainThread(object state)
        {
            for (int i = 0; i < 5; i++)
                Console.WriteLine("Hello Thread!");
        }

        static void Main(string[] args)
        {
            // Thread를 최소 1개에서 최대 5개까지만 빌려줄 수 있도록 제한
            ThreadPool.SetMinThreads(1, 1);
            ThreadPool.SetMaxThreads(5, 5);

            // 람다식을 통해 Pool에서 Thread를 빌린뒤 영원히 반환하지 않는 함수 생성
            for (int i = 0; i < 5; i++)
                ThreadPool.QueueUserWorkItem((obj) => { while (true) { } });

            // Thread Pool에 더이상 남아있는 Thread가 없어 실행 X
            ThreadPool.QueueUserWorkItem(MainThread);
        }
    }
}​

 

Task 응용
using System;
using System.Threading;
using System.Threading.Tasks; // 추가해야 Task 사용 가능

namespace ServerCore
{
    class Program
    {
        static void MainThread(object state)
        {
            for (int i = 0; i < 5; i++)
                Console.WriteLine("Hello Thread!");
        }

        static void Main(string[] args)
        {
            // Thread를 최소 1개에서 최대 5개까지만 빌려줄 수 있도록 제한
            ThreadPool.SetMinThreads(1, 1);
            ThreadPool.SetMaxThreads(5, 5);
            
            for (int i = 0; i < 5; i++)
            {
                // Task로 직원이 할 일감을 생성하여 던져주면, ThreadPool에 대기중인 Thread가 해당 일감을 실행
                // Task 생성시 TaskCreationOptions.LongRunning 옵션을 설정할 경우 Thread를 따로 관리
                Task t = new Task(() => { while (true) { } }, TaskCreationOptions.LongRunning);
                t.Start();
            }

            // TaskCreationOptions.LongRunning 옵션을 통해 Thread를 따로 관리하므로 실행 가능
            ThreadPool.QueueUserWorkItem(MainThread);
        }
    }
}

 

# 컴파일러 최적화

 

- Debug 모드와 Release 모드의 가장 큰 차이점은 Debug 모드에서는 코드 최적화를 하지 않고, Release 모드에서는 코드

  최적화를 한다는 것이다.

 ~> 따라서 Debug 모드에서는 잘 실행되던 코드가 Release 모드에서는 실행되지 않을 수 있다.

 ~> volatile Keyword를 통해 최적화를 금지시킬 수 있다.

volatile Keyword 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        // volatile Keyword를 통해 해당 변수에 대한 최적화를 금지시킨다.
        volatile static bool _stop = false; // 전역 변수는 모든 Thread들이 공유

        static void ThreadMain()
        {
            Console.WriteLine("Thread 시작!");

            // Debug 모드가 아닌 Release 모드에서는 코드 최적화로 인해 일어나지 않던 Bug들이 발생할 수 있다.
            while (_stop == false)
            {
                // 누군가가 stop 신호를 true로 바꿔주기를 기다린다.
            }

            // 위의 while문은 컴파일러 최적화시 아래의 코드와 같이 변경된다. ( 즉, 없던 Bug 발생 )
            //if (_stop == false)
            //{
            //    while (true)
            //    {
                      // 무한 루프에 빠진다.
            //    }
            //}

            Console.WriteLine("Thread 종료!");
        }

        static void Main(string[] args)
        {
            Task t = new Task(ThreadMain);
            t.Start();

            Thread.Sleep(1000); // 1초동안 일시정지

            _stop = true;
            Console.WriteLine("Stop 호출!");

            Console.WriteLine("종료 대기중");
            t.Wait(); // 해당 Task가 종료될때까지 기다리는 것 ( Thread의 Join() 과 같은 기능 )
            Console.WriteLine("종료 성공");
        }
    }
}​

 

# 캐시 이론

 

- 직원이 서빙을 통해 주문을 받고 주방과 원격으로 연결된 주문 현황에 주문 내역을 입력할 경우 주방에서 조리를

  시작한다. ( 이때 직원이 주문 현황을 입력하기 위한 동선 자체가 많이 길다고 가정 )

- 직원은 보다 더 효율적으로 움직이기 위해서 주문을 1개 받자마자 멀리 떨어진 주문 현황에 주문 내역을 입력하러

  가는 것이 아닌, 단기 기억/미니 수첩/대형 수첩 을 활용하여 주문들을 전부 기록해둔 뒤 나중에 한꺼번에 입력을 한다.

 ~> 만약 손님이 주문은 번복할 경우 본인이 수첩에 기록해둔 주문만 수정하면 되기 때문에 보다 더 효율적이다.

 

- 그러나 이러한 방법이 MultiThread 에서는 골치아플 수 있다.

 ~> 직원 1이 2번 테이블에 대한 콜라 주문을 받고, 2번 테이블 손님들이 직원 2에게 콜라가 아닌 사이다를 달라고

      주문을 번복할 경우 혼란이 발생한다.

 ~> 직원 2 입장에서는 본인이 2번 테이블에 대해 콜라 주문을 받지도 않았고, 확인하고자 주문 현황을 확인해봐도

      2번 테이블이 콜라를 주문했다는 사실을 알 수 없다.

     ( 2번 테이블에 대한 콜라 주문은 아직까지는 직원 1의 미니 수첩에 기록됨 )

 

- 위의 내용에서 로봇 직원은 Thread, 단기 기억/미니 수첩/대형 수첩이 Cache, 주문 현황은 Main Memory에 해당한다.

 

- 캐시 철학으로 시간적 지역성, 공간적 지역성이 있다.

 ~> 시간적 지역성은 최근에 참조된 주소의 내용이 다시 참조될 가능성이 높다는 것이다.

 ~> 공간적 지역성은 기억장치 내에 서로 인접하여 저장되어 있는 데이터들이 연속적으로 접근될 가능성이 높다는 것이다.

캐시 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static void Main(string[] args)
        {
            int[,] arr = new int[10000, 10000];

            {
                long start = DateTime.Now.Ticks;
                for (int x = 0; x < 10000; x++)
                    for (int y = 0; y < 10000; y++)
                        arr[x, y] = 1;
                long end = DateTime.Now.Ticks;
                Console.WriteLine($"(x, y) 순서 걸린 시간 {end - start}"); // 결과 : 3138744
            }

            {
                long start = DateTime.Now.Ticks;
                for (int x = 0; x < 10000; x++)
                    for (int y = 0; y < 10000; y++)
                        arr[y, x] = 1;
                long end = DateTime.Now.Ticks;
                Console.WriteLine($"(y, x) 순서 걸린 시간 {end - start}"); // 결과 : 4003940
            }
        }
    }
}​

 

# 메모리 배리어

- 코드 최적화 뿐만이 아니라 HW 역시 최적화를 진행한다.

 ~> CPU에게 일련의 명령어들을 시킬 경우 CPU가 의존성이 없다고 판단되는 명령어들의 순서를 제멋대로 재배치한다.

 ~> Memory Barrier를 통해 코드 순서 재배치를 억제할 수 있다.

- Memory Barrier를 통해 가시성 또한 확보할 수 있다.

 ~> 이때의 가시성 확보는 MultiThread 환경에서 Thread의 Cache의 상황을 Main Memory에 반영하는 것을 말한다.

 ~> 따라서 Store 후에 1번, Load 전에 1번 해주는 것이 가시성 확보에 도움이 된다. ( 최신 정보 반영 및 사용 )

HW 최적화 예시
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static int x = 0;
        static int y = 0;
        static int result1 = 0;
        static int result2 = 0;

        static void Thread_1()
        {
            // CPU가 아래의 명령어들이 의존성이 없다고 판단하여 코드의 순서를 제멋대로 재배치
            y = 1; // Store y
            result1 = x; // Load x
        }

        static void Thread_2()
        {
            // CPU가 아래의 명령어들이 의존성이 없다고 판단하여 코드의 순서를 제멋대로 재배치
            x = 1; // Store x
            result2 = y; // Load y
        }

        static void Main(string[] args)
        {
            int count = 0;
            while (true)
            {
                count++;
                x = y = result1 = result2 = 0;

                Task t1 = new Task(Thread_1);
                Task t2 = new Task(Thread_2);
                t1.Start();
                t2.Start();

                Task.WaitAll(t1, t2); // 2개의 Thread가 전부 끝날때까지 Main Thread는 대기

                // CPU가 코드의 순서를 제멋대로 재배치하여 둘 다 0인 상황이 발생할 수 있었다.
                if (result1 == 0 && result2 == 0)
                    break;
            }

            Console.WriteLine($"{count}번만에 빠져나옴!");
        }
    }
}​

 

Memory Barrier 응용 ( 코드 순서 재배치 억제, 가시성 )
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    // 메모리 배리어 기능
    // A) 코드 순서 재배치 억제
    // B) 가시성 확보 (MultiThread 환경에서 Thread의 Cache의 상황을 Main Memory에 반영)

    // 메모리 배리어 종류
    // 1) Full Memory Barrier (어셈블리어 : MFENCE, C# : Thread.MemoryBarrier) : Store/Load 둘 다 막는다.
    // 2) Store Memory Barrier (어셈블리어 : SFENCE) : Store 만 막는다.
    // 3) Load Memory Barrier (어셈블리어 : LFENCE) : Load 만 막는다.

    class Program
    {
        static int x = 0;
        static int y = 0;
        static int result1 = 0;
        static int result2 = 0;

        static void Thread_1()
        {
            y = 1; // Store y ( Main Memory에 y의 값을 반영 )

            Thread.MemoryBarrier(); // 코드 순서 재배치 억제

            result1 = x; // Load x ( Main Memory에 있는 x의 값을 반영 )
        }

        static void Thread_2()
        {
            x = 1; // Store x ( Main Memory에 x의 값을 반영 )

            Thread.MemoryBarrier(); // 코드 순서 재배치 억제

            result2 = y; // Load y ( Main Memory에 있는 y의 값을 반영 )
        }

        static void Main(string[] args)
        {
            int count = 0;
            while (true)
            {
                count++;
                x = y = result1 = result2 = 0;

                Task t1 = new Task(Thread_1);
                Task t2 = new Task(Thread_2);
                t1.Start();
                t2.Start();

                Task.WaitAll(t1, t2); // 2개의 Thread가 전부 끝날때까지 Main Thread는 대기

                // CPU의 코드 재배치를 억제하였기 때문에 둘 다 0인 경우는 존재 X ~> 무한루프
                if (result1 == 0 && result2 == 0)
                    break;
            }

            Console.WriteLine($"{count}번만에 빠져나옴!");
        }
    }
}

 

# interlocked

- Race Condition (경합 조건) 이란 2개 이상의 Process 혹은 Thread가 공유 자원을 서로 사용하려고 경합(Race)하는

  현상을 의미한다.

- MultiThread 환경에서는 Process 내의 모든 자원을 공유할 수 있다는 점에서 동기화 문제가 발생한다.

 ~> Interlocked 를 통해 해결이 가능하다. ( 메모리 동기화 구현 )

 ~> Interlocked 는 정수만 사용할 수 있다는 단점이 존재한다.

Race Condition 예시
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static int number = 0;

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
                number++;

            // number++은 어셈블리어에서 아래와 같이 동작한다. (즉, atomic operation이 X)
            // int temp = number;
            // temp += 1;
            // number = temp;
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
                number--;

            // number--은 어셈블리어에서 아래와 같이 동작한다. (즉, atomic operation이 X)
            // int temp = number;
            // temp -= 1;
            // number = temp;
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            // 위의 주석 처리된 6줄의 코드 순서를 정확히 알지 못하기 때문에 결과 예측 X
            Console.WriteLine(number);
        }
    }
}​

 

Interlocked 를 통한 원자성 및 순서 보장
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static int number = 0;

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
                Interlocked.Increment(ref number); // 원자성 보장 및 순서 보장 (All or Nothing)
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
                Interlocked.Decrement(ref number); // 원자성 보장 및 순서 보장 (All or Nothing)
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(number);
        }
    }
}​

 

# Lock 기초

- Critical Section (임계 영역) 은 2개 이상의 Thread가 동시에 접근할 수 없는 공유 자원을 사용하는 코드 영역을 의미한다.

- Mutual Exclusive (상호 배제) 란 특정 시점에서 공유 자원을 1개의 Thread 만이 사용할 수 있으며, 다른 Thread 들은

  접근하지 못하도록 제어하는 방법을 의미한다.

Critical Section 및 Mutual Exclusive 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static int number = 0;
        static object _obj = new object();

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                Monitor.Enter(_obj); // 문에 잠금장치 설정 (이미 잠겨있을 경우 대기)
                number++;
                Monitor.Exit(_obj); // 문에 잠금장치 해제 (생략할 경우 deadlock 발생 가능)
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                Monitor.Enter(_obj); // 문에 잠금장치 설정 (이미 잠겨있을 경우 대기)
                number--;
                Monitor.Exit(_obj); // 문에 잠금장치 해제 (생략할 경우 deadlock 발생 가능)
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(number);
        }
    }
}​

 

lock Keyword 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static int number = 0;
        static object _obj = new object();

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                lock(_obj) // 내부는 Monitor.Enter 과 Monitor.Exit 로 구현됨 (자동으로 lock 해제)
                {
                    number++;
                }
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                lock (_obj) // 내부는 Monitor.Enter 과 Monitor.Exit 로 구현됨 (자동으로 lock 해제)
                {
                    number--;
                }
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(number);
        }
    }
}​

 

# DeadLock

- Dead Lock (교착 상태) 이란 2개 이상의 Process 또는 Thread가 Mutual Exclusive 으로 사용하던 자원을 요청하면서

  서로가 가진 자원을 대기하는 현상을 말한다.

- Dead Lock을 try-catch문, Monitor.TryEnter()를 통해 예외를 처리하기보단 Crash 를 통해 오류를 수정하는 것이 더 좋다.

- Dead Lock은 개발 단계에서 잘 발생하지 않다가 출시 이후 User들이 몰리는 상황에 갑작스럽게 발생하는 경우가 많다.

Dead Lock 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class SessionManager
    {
        static object _lock = new object();

        public static void TestSession()
        {
            lock (_lock)
            {

            }
        }

        public static void Test()
        {
            lock (_lock)
            {
                UserManager.TestUser();
            }
        }
    }

    class UserManager
    {
        static object _lock = new object();

        public static void TestUser()
        {
            lock (_lock)
            {

            }
        }

        public static void Test()
        {
            lock (_lock)
            {
                SessionManager.TestSession();
            }
        }
    }

    class Program
    {
        static void Thread_1()
        {
            for (int i = 0; i < 10000; i++)
            {
                SessionManager.Test();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 10000; i++)
            {
                UserManager.Test();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);
            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);
        }
    }
}​

 

# Lock 구현 이론

- Lock 요청시 이미 누가 Lock을 점유하고 있을 경우 이를 어떻게 처리하는지에 따라 성능이 결정된다.

 1. 루프를 돌면서 계속 점유를 시도한다. ( SpinLock )

 2. Thread가 자신의 소유권을 포기하고, 나중에 다시 lock을 요청한다. ( 랜덤성 / context switching 비용 발생 )

 3. 운영체제에게 lock이 해제될 경우 본인에게 통보해달라고 요청한다. ( AutoResetEvent )

 

# SpinLock

오류가 발생하는 SpinLock 구현 (즉, 루프를 돌면서 계속 점유를 시도)
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class SpinLock
    {
        volatile bool _locked = false; // lock 잠금 상태 (해당 lock의 사용 유무)

        public void Acquire() // lock 설정
        {
            // 오류가 발생하는 이유는 lock 잠금 상태 확인과 lock을 잠그는 것을 따로 하고 있기 때문
            // 만약 2개의 Thread가 거의 동시다발적으로 들어와 동시에 while 문을 통과할 경우 문제 발생
            // 따라서 아래의 lock 잠금 상태 확인과 lock을 잠그는 코드를 하나로 묶어줘야 한다.

            // 오류 발생 원인 시작
            while (_locked)
            {
                // 잠금 상태가 풀릴때까지 무작정 기다린다.
            }

            _locked = true; 
            // 오류 발생 원인 종료
        }

        public void Release() // lock 해제
        {
            _locked = false;
        }
    }

    class Program
    {
        static int _num = 0;
        static SpinLock _lock = new SpinLock();

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}​

 

정상적으로 동작하는 SpinLock 구현 (즉, 루프를 돌면서 계속 점유를 시도)
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class SpinLock
    {
        volatile int _locked = 0; // lock 잠금 상태 (해당 lock의 사용 유무)

        public void Acquire() // lock 설정
        {
            while (true)
            {
                // 방법 #1
                // Interlocked.Exchange는 1번째 인자의 값을 2번째 인자의 값으로 변경하며, 반환값은 1번째 인자의 변경 전 값이다.
                int original = Interlocked.Exchange(ref _locked, 1); 
                if (original == 0) // 즉, 아무도 lock을 사용하지 않는 상황에서 내가 lock을 건 경우
                    break; // 무작정 기다리는 것을 그만두겠다. (즉, lock 휙득 성공)

                // 방법 #2
                // Interlocked.CompareExchange는 1, 3번째 인자의 값이 같다면 2번째 인자의 값을 1번째 인자에 대입하며, 반환값은 1번째 인자의 변경 전 값이다.
                // Interlocked.CompareExchange는 CAS (Compare-And-Swap) 연산 수행
                int expected = 0; // 예상한 값
                int desired = 1; // 기대한 값
                int original2 = Interlocked.CompareExchange(ref _locked, desired, expected);
                if (original2 == 0)  // 즉, 아무도 lock을 사용하지 않는 상황에서 내가 lock을 건 경우
                    break;  // 무작정 기다리는 것을 그만두겠다. (즉, lock 휙득 성공)
            }
        }

        public void Release() // lock 해제
        {
            _locked = 0;
        }
    }

    class Program
    {
        static int _num = 0;
        static SpinLock _lock = new SpinLock();

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}​

 

# Context Switching

- 루프를 돌면서 계속 점유를 시도하는 SpinLock과 달리, Sleep과 Yield를 통해 Thread가 자신의 소유권을 포기하고,

  나중에 다시 lock을 요청하는 lock 구현 방식이 마냥 장점만 존재한다고는 할 수 없다.

 ~> Context Switching 비용 문제가 발생한다.

- Context Switching 이란 CPU가 어떤 Process 또는 Thread를 실행하고 있는 상태에서 운영체제의 Scheduling에 의해

  더 높은 우선순위를 가진 Process 또는 Thread가 실행되어야 할 때, Register에 저장된 기존 Process 또는 Thread의 정보

  를 Kernel에 저장하고 실행하고자 하는 Process 또는 Thread의 정보를 복원하는 일련의 과정을 말하며 이러한 과정은

  lock 구현때뿐만이 아닌 늘상 발생하고 있는 자연스러운 현상이다.

Sleep과 Yield 를 통한 lock 구현 (즉, Thread가 자신의 소유권을 포기하고, 나중에 다시 lock을 요청)
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Lock
    {
        volatile int _locked = 0; // lock 잠금 상태 (해당 lock의 사용 유무)

        public void Acquire() // lock 설정
        {
            while (true)
            {
                // 방법 #1
                int original = Interlocked.Exchange(ref _locked, 1);
                if (original == 0)
                    break;

                // 방법 #2
                int expected = 0;
                int desired = 1;
                int original2 = Interlocked.CompareExchange(ref _locked, desired, expected);
                if (original2 == 0)
                    break;

                // 잠시 쉬다 올게~ (3개의 방법중 1개 선택)
                Thread.Sleep(1); // 무조건 휴식 => 1ms 정도 쉬고 싶은데 정확한 시간은 운영체제가 결정
                Thread.Sleep(0); // 조건부 양보 => 나보다 우선순위가 낮은 애들한테는 양보 X => 나보다 우선순위가 높거나 같은 애들이 없다면 남은 시간을 본인이 소진
                Thread.Yield(); // 관대한 양보 => 관대하게 양보할테니, 지금 실행이 가능한 애가 있다면 실행하세요 => 실행 가능한 애가 없다면 남은 시간을 본인이 소진
            }
        }

        public void Release() // lock 해제
        {
            _locked = 0;
        }
    }

    class Program
    {
        static int _num = 0;
        static Lock _lock = new Lock();

        static void Thread_1()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 100000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}​

 

# AutoResetEvent

AutoResetEvent를 통한 lock 구현 (즉, 운영체제에게 lock이 해제될 경우 본인에게 통보해달라고 요청)
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Lock
    {
        // bool <- Kernel
        AutoResetEvent _avaliable = new AutoResetEvent(true); // true일 경우 아무나 들어올 수 있는 상태, false일 경우 누구도 들어올 수 없는 상태

        public void Acquire() // lock 설정
        {
            _avaliable.WaitOne(); // 입장 시도 (true일 경우 입장이 가능하며, 자동으로 문을 닫아준다.)
            // _avaliable.Reset()은 bool의 값을 false로 변경 -> AutoResetEvent에서는 WaitOne() 내부에 포함되어 있다.
        }

        public void Release() // lock 해제
        {
            _avaliable.Set(); // bool의 값을 true로 변경
        }
    }

    class Program
    {
        static int _num = 0;
        static Lock _lock = new Lock();

        static void Thread_1()
        {
            for (int i = 0; i < 1000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 1000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}​

 

오류가 발생하는 ManualResetEvent를 통한 lock 구현
(즉, 운영체제에게 lock이 해제될 경우 본인에게 통보해달라고 요청)
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Lock
    {
        // bool <- Kernel
        ManualResetEvent _avaliable = new ManualResetEvent(true); // true일 경우 아무나 들어올 수 있는 상태, false일 경우 누구도 들어올 수 없는 상태

        public void Acquire() // lock 설정
        {
            // 오류가 발생하는 이유는 입장 시도와 문을 닫는 것을 따로 하고 있기 때문
            _avaliable.WaitOne(); // 입장 시도 (true일 경우 입장이 가능하며, 자동으로 문을 닫아주지 않는다.)
            _avaliable.Reset();  // bool의 값을 false로 변경 -> ManualResetEvent에서는 WaitOne() 내부에 포함되어 있지 않다.
        }

        public void Release() // lock 해제
        {
            _avaliable.Set(); // bool의 값을 true로 변경
        }
    }

    class Program
    {
        static int _num = 0;
        static Lock _lock = new Lock();

        static void Thread_1()
        {
            for (int i = 0; i < 1000; i++)
            {
                _lock.Acquire();
                _num++;
                _lock.Release();
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 1000; i++)
            {
                _lock.Acquire();
                _num--;
                _lock.Release();
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}​

 

Mutex를 통한 lock 구현
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static int _num = 0;
        static Mutex _lock = new Mutex();

        static void Thread_1()
        {
            for (int i = 0; i < 1000; i++)
            {
                _lock.WaitOne(); // 입장 시도 (입장 성공시 문을 잠근다.)
                _num++;
                _lock.ReleaseMutex(); // 나가면서 문의 잠금을 해제한다.
            }
        }

        static void Thread_2()
        {
            for (int i = 0; i < 1000; i++)
            {
                _lock.WaitOne(); // 입장 시도 (입장 성공시 문을 잠근다.)
                _num--;
                _lock.ReleaseMutex(); // 나가면서 문의 잠금을 해제한다.
            }
        }

        static void Main(string[] args)
        {
            Task t1 = new Task(Thread_1);
            Task t2 = new Task(Thread_2);

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(_num);
        }
    }
}

 

+ 추가 정리

- ManualResetEvent 사용 이유?

 ~> ManualResetEvent는 1개의 Thread만 통과시키고 문을 닫는 AutoResetEvent와 달리, 문이 1번 열리면 대기중이던

      여러개의 Thread 를 동시에 수행하고자 할 때 유용하다.

- AutoResetEvent와 Mutex의 차이?

 ~> Mutex는 AutoResetEvent 보다 더 많은 정보를 가진다. (잠금 횟수, Thread ID 등...) 이에 따른 추가비용도 존재한다.

 

# ReaderWriterLock

ReaderWriterLock 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

        // 예를 들어 게임에서 일일퀘스트 보상 지급시 코인, 경험치, 물약은 고정적인 보상이며
        // 주말, 명절 이벤트로 일일 퀘스트 보상에 +a 정도의 보상이 추가로 지급된다고 가정하자.
        // 보상을 얻는 함수는 수없이 실행될거고, 보상을 추가하는 함수는 가끔 실행될 것이다.
        // 따라서 보상을 추가하는 함수의 lock은 실행될 확률이 1.0%, 실행되지 않을 확률이 99.0% 이다.
        // 이럴때 쓰는 lock이 ReaderWriterLock이다. (ReaderWriterLockSlim이 더 최신버전)

        class Reward
        {

        }

        static Reward GetRewardById(int id)
        {
            _lock.EnterReadLock(); // 아무도 WriteLock 을 잡고 있지 않은 경우에 마치 lock이 없는것처럼 Thread들이 동시다발적으로 들어올 수 있다.

            _lock.ExitReadLock();

            return null;
        }

        void AddReward(Reward reward)
        {
            _lock.EnterWriteLock(); // 1번에 1개의 Thread만 휙득할 수 있다.

            _lock.ExitWriteLock();
        }


        static void Main(string[] args)
        {

        }
    }
}

 

+ 추가 검색 ( https://rito15.github.io/posts/06-cs-reader-writer-lock/ )

- Thread 간에 공유되는 데이터가 존재할 경우, 항상 모든 Thread가 해당 데이터를 읽고 쓰는 것은 아니다.

 ~> 어떤 Thread는 해당 데이터를 읽기만 하고, 어떤 Thread는 해당 데이터를 쓰기만 할 수도 있다.

 ~> 또한 다수의 읽기 Thread, 소수의 쓰기 Thread가 수행되는 경우도 존재한다.

 ~> 이럴때 일반적인 lock을 구현하여 읽기/쓰기가 수행되는 동안에 항상 lock을 설정/해제할 경우

      데이터를 단순히 읽기만 하여 값이 변경되지 않는 상황에도 불필요하게 Critical Section을 만들게 되므로

      성능 관점에서 손해다.

 ~> ReaderWriterlock 을 통해 해결이 가능하다.

- ReaderWriterlock은 데이터에 대한 쓰기 작업시에만 lock을 설정하고 읽기 작업시에는 lock을 설정하지 않는

  비대칭적인 lock을 구현하여 성능 관점에서 이득을 취했다.

 

# ReaderWriterLock 구현 연습

재귀적 lock을 허용하지 않는 ReaderWriterLock 구현
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    // 재귀적 lock을 허용할지 (No)
    // Spinlock 정책 (5000번 Spin 후 Yield)
    class Lock
    {
        const int EMPTY_FLAG = 0x00000000;
        const int WRITE_MASK = 0X7FFF0000; // WRITE 부분만 추출하기 위한 MASK (AND 연산을 통한 추출)
        const int READ_MASK = 0x0000FFFF; // READ 부분만 추출하기 위한 MASK (AND 연산을 통한 추출)
        const int MAX_SPIN_COUNT = 5000;

        // [ Unused (1bit) ] [ WriteThreadId (15bit) ] [ ReadCount (16bit) ] ~> 32bit (int형)
        // Unused : 최상위 bit는 부호 bit이므로 음수가 될 가능성이 있어 사용 X / WriteThreadId : Write lock을 가진 Thread의 Id / ReadCount : 읽기 Thread의 총 수
        int _flag = EMPTY_FLAG;

        public void WriteLock()
        {
            // 아무도 WriteLock of ReadLock을 휙득하고 있지 않을 때, 경합해서 소유권을 얻는다.
            int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK; // WriteThreadId (15bit)에는 본인의 Thread Id를 , WriteThreadId (15bit)를 제외한 나머지 bit는 전부 0으로 설정
            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    // 시도를 해서 성공하면 return
                    if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                        return;
                }

                Thread.Yield();
            }
        }

        public void WriteUnlock()
        {
            Interlocked.Exchange(ref _flag, EMPTY_FLAG);
        }

        public void ReadLock()
        {
            // 아무도 WriteLock을 휙득하고 있지 않을 때, ReadCount를 1 늘린다.
            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    int expected = (_flag & READ_MASK); // READ_MASK와 AND 연산시 WRITE 부분도 0 -> 아무도 WriteLock을 휙득하고 있지 X
                    if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
                        return;

                    // 위의 연산이 실패하는 경우 ?
                    // #1. 누군가 WriteLock을 휙득하고 있는 경우
                    // #2. A와 B Thread가 거의 동시에 도착하여 expected의 값은 동일하게 얻지만,
                    //     A Thread가 먼저 CompareExchange 를 통해 expected의 값을 +1 할 경우
                    //     B Thread가 CompareExchange 시 _flag의 값이 expected + 1 로 변경되어 요청이 실패한다.
                }

                Thread.Yield();
            }
        }

        public void ReadUnlock()
        {
            Interlocked.Decrement(ref _flag);
        }
    }
}​

 

재귀적 lock을 허용하는 ReaderWriterLock 구현
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    // 재귀적 lock을 허용할지 (Yes) : WriteLock ~> WriteLock (OK), WriteLock ~> ReadLock (OK), ReadLock ~> WriteLock (Not OK)
    class Lock
    {
        const int EMPTY_FLAG = 0x00000000;
        const int WRITE_MASK = 0X7FFF0000;
        const int READ_MASK = 0x0000FFFF;
        const int MAX_SPIN_COUNT = 5000;

        int _flag = EMPTY_FLAG;
        int _writeCount = 0; // 재귀적으로 몇개의 Write를 할 것인지

        public void WriteLock()
        {
            // 동일 Thread가 WriteLock을 이미 휙득하고 있는지 확인
            int lockThreadId = (_flag & WRITE_MASK);
            if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
            {
                _writeCount++;
                return;
            }

            // 아무도 WriteLock of ReadLock을 휙득하고 있지 않을 때, 경합해서 소유권을 얻는다.
            int desired = (Thread.CurrentThread.ManagedThreadId << 16) & WRITE_MASK; // WriteThreadId (15bit)에는 본인의 Thread Id를 , WriteThreadId (15bit)를 제외한 나머지 bit는 전부 0으로 설정
            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    // 시도를 해서 성공하면 return
                    if (Interlocked.CompareExchange(ref _flag, desired, EMPTY_FLAG) == EMPTY_FLAG)
                    {
                        _writeCount = 1;
                        return;
                    }
                }

                Thread.Yield();
            }
        }

        public void WriteUnlock()
        {
            int lockCount = --_writeCount;
            if (lockCount == 0)
                Interlocked.Exchange(ref _flag, EMPTY_FLAG);
        }

        public void ReadLock()
        {
            // 동일 Thread가 WriteLock을 이미 휙득하고 있는지 확인
            int lockThreadId = (_flag & WRITE_MASK);
            if (Thread.CurrentThread.ManagedThreadId == lockThreadId)
            {
                Interlocked.Increment(ref _flag);
                return;
            }

            // 아무도 WriteLock을 휙득하고 있지 않을 때, ReadCount를 1 늘린다.
            while (true)
            {
                for (int i = 0; i < MAX_SPIN_COUNT; i++)
                {
                    int expected = (_flag & READ_MASK); // READ_MASK와 AND 연산시 WRITE 부분도 0 -> 아무도 WriteLock을 휙득하고 있지 X
                    if (Interlocked.CompareExchange(ref _flag, expected + 1, expected) == expected)
                        return;
                }

                Thread.Yield();
            }
        }

        public void ReadUnlock()
        {
            Interlocked.Decrement(ref _flag);
        }
    }
}​

 

구현한 ReaderWriterLock 동작 결과 확인
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        static volatile int count = 0;
        static Lock _lock = new Lock();

        static void Main(string[] args)
        {
            Task t1 = new Task(delegate ()
            {
                for (int i = 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    count++;
                    _lock.WriteUnlock();
                }
            });

            Task t2 = new Task(delegate ()
            {
                for (int i = 0; i < 100000; i++)
                {
                    _lock.WriteLock();
                    count--;
                    _lock.WriteUnlock();
                }
            });

            t1.Start();
            t2.Start();

            Task.WaitAll(t1, t2);

            Console.WriteLine(count);
        }
    }
}​

 

# Thread Local Storage

 

- MultiThread 환경에서 수많은 Thread가 하나의 자원에 접근하고자 할 때 lock은 상호 배타적인 개념이기 때문에

  1번에 1개의 작업만을 처리할 수 있다. 이에 MultiThread 환경의 장점을 활용하지 못할뿐더러 하나의 Thread가 처리하는 

  속도보다 못할 수도 있다.

 ~> Thread Local Storage를 통해 해결이 가능하다.

- Thread Local Storage는 전역변수이지만, Thread 마다 고유하게 접근할 수 있는 전역 변수이다.

Thread Local Storage 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        // Thread 마다 고유하게 접근할 수 있는 전역변수
        static ThreadLocal<string> ThreadName = new ThreadLocal<string>();

        static void WhoAmI()
        {
            ThreadName.Value = $"My Name Is {Thread.CurrentThread.ManagedThreadId}";

            Thread.Sleep(1000);

            Console.WriteLine(ThreadName.Value);
        }

        static void Main(string[] args)
        {
            Parallel.Invoke(WhoAmI, WhoAmI, WhoAmI, WhoAmI); // 매개변수로 입력하는 Action 만큼 Task를 만들어준다.

            ThreadName.Dispose(); // 자원 해제
        }
    }
}​

 

보다 성능을 개선한 Thread Local Storage 응용
using System;
using System.Threading;
using System.Threading.Tasks;

namespace ServerCore
{
    class Program
    {
        // Thread 마다 고유하게 접근할 수 있는 전역변수
        // new ThreadLocal의 인자로 Func delegate를 받을 수 있다.
        // 이를 통해 TLS가 새로 만들어질 경우 ThreadLocal.Value에 return 값을 넣어준 뒤 그대로 보관한다.
        static ThreadLocal<string> ThreadName = new ThreadLocal<string>(() => { return $"My Name Is {Thread.CurrentThread.ManagedThreadId}"; });

        static void WhoAmI()
        {
            // IsValueCreated는 ThreadLocal.Value의 값이 초기화 된 경우 true, 그렇지 않을 경우 false를 반환한다.  
            bool repeat = ThreadName.IsValueCreated;
            if (repeat)
                Console.WriteLine(ThreadName.Value + " (repeat)");
            else
                Console.WriteLine(ThreadName.Value);
        }

        static void Main(string[] args)
        {
            ThreadPool.SetMinThreads(1, 1);
            ThreadPool.SetMaxThreads(3, 3);
            Parallel.Invoke(WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI, WhoAmI); // 매개변수로 입력하는 Action 만큼 Task를 만들어준다.

            ThreadName.Dispose(); // 자원 해제
        }
    }
}​

 

[ 섹션 2. 네트워크 프로그래밍 ]

# 네트워크 기초 이론

 

- 삼성 아파트 201호에 사는 사람이 삼성 아파트 102호에 사는 사람에게 택배를 보내고자 한다.

- 삼성 아파트 202호에 사는 사람이 현대 아파트 101호에 사는 사람에게 택배를 보내고자 한다.

 ~> 이때 본인이 직접 택배를 배송하는 것이 아닌 우선 경비실에 본인이 보내고자 하는 택배를 전달한다.

 ~> 경비실에서 보내는 사람과 받는 사람을 확인한 뒤, 만약 두 사람이 사는 아파트가 같다면 경비 아저씨가 직접

      택배를 전달해주고, 같지 않다면 택배 배송센터로 해당 택배를 전달한다.

 ~> 택배 배송 센터에서 받는 사람이 거주하는 아파트의 경비실에 택배를 전달하면, 경비아저씨가 직접 택배를

      전달해준다.

- 위의 내용에서 고객은 단말기, 경비실은 스위치, 택배 배송센터는 라우터, 아파트 단지는 네트워크에 해당한다.

 

# 통신 모델

 

- 게임의 규모, 장르, 개발자의 역량에 따라 Protocol을 선택하는 것이 중요하다.

 ~> MMO RPG는 주로 TCP, Latency가 중요한 FPS는 주로 UDP를 사용한다.

 ~> TCP는 느린 속도와 높은 신뢰성을 가지고, UDP는 빠른 속도와 낮은 신뢰성을 가진다.

 

# 소켓 프로그래밍 입문 #1

 

- 손님은 본인의 핸드폰을 통해 식당에 전화를 걸어 입장이 가능한지 물어보고, 만약 입장이 가능한 경우 본인의 대리인을

  식당에 입장 시킨다. 손님은 대리인을 통해 식당과 대화가 가능하다.

- 식당은 우선 문지기를 고용하고, 해당 문지기에게 손님들의 입장 문의 전화를 받을 수 있는 식당 번호를 알려준다.

  문지기는 손님 대리인을 통해 손님과 대화가 가능하다.

- 직원 고용 및 교육이 끝난 경우 식당은 영업 및 안내를 시작한다.

 

 

- 위의 내용에서 손님은 Client, 대리인은 Session, 식당은 Server, 문지기는 Listener 소켓, 문지기 교육은 Bind,

  영업 시작은 Listen, 안내는 Accept에 해당한다.

 

# 소켓 프로그래밍 입문 #2

 

- 우선 Test를 위해 [ 솔루션 ] - [ 오른쪽 마우스 ] - [ 속성 ] 의 [ 공용 속성 ] - [ 시작 프로젝트 ] 를 다음과 같이 설정한다.

Server 코드 구현
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    class Program
    {
        static void Main(string[] args)
        {
            // DNS (Domain Name System) : Domain을 IP 네트워크에서 찾아갈 수 있는 IP로 변환해 준다. 
            string host = Dns.GetHostName(); // Local Computer의 host 이름을 반환
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; // ip 주소를 배열로 반환 (예를 들어 Google과 같이 Traffic이 어마무시한 사이트는 여러개의 ip 주소를 가질 수 있기 때문)
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip 주소와 port 번호를 매개변수로 입력

            // 문지기 고용
            Socket listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                // 문지기 교육
                listenSocket.Bind(endPoint);

                // 영업 시작
                // backlog : 최대 대기 수
                listenSocket.Listen(10); // backlog를 매개변수로 입력

                while (true)
                {
                    Console.WriteLine("Listening...");

                    // 손님을 입장시킨다
                    // Accept() 는 Blocking 함수이므로 Client가 입장하지 않을 경우 무한히 대기한다.
                    Socket clientSocket = listenSocket.Accept(); // 이때의 clientSocket은 대리인에 해당한다.

                    // 받는다
                    byte[] recvBuff = new byte[1024];
                    int recvBytes = clientSocket.Receive(recvBuff); // 몇 byte를 받았는지 int로 반환
                    string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes); // 어디에서, 어디부터, 얼만큼 받아올 것인지를 매개변수로 입력
                    Console.WriteLine($"[From Client] {recvData}");

                    // 보낸다
                    byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                    clientSocket.Send(sendBuff); // 대리인을 통해 Client와 대화

                    // 쫓아낸다
                    clientSocket.Shutdown(SocketShutdown.Both); // 연결 해제를 미리 예고
                    clientSocket.Close();
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}

 

Client 코드 구현
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace DummyClient
{
    class Program
    {
        static void Main(string[] args)
        {
            // DNS (Domain Name System) : Domain을 IP 네트워크에서 찾아갈 수 있는 IP로 변환해 준다. 
            string host = Dns.GetHostName(); // Local Computer의 host 이름을 반환
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; // ip 주소를 배열로 반환 (예를 들어 Google과 같이 Traffic이 어마무시한 사이트는 여러개의 ip 주소를 가질 수 있기 때문)
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip 주소와 port 번호를 매개변수로 입력

            // 휴대폰 준비
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            try
            {
                // 입장 문의
                socket.Connect(endPoint);
                Console.WriteLine($"Connected To {socket.RemoteEndPoint.ToString()}"); // RemoteEndPoint는 본인과 연결된 대상

                // 보낸다
                byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World");
                socket.Send(sendBuff);

                // 받는다
                byte[] recvBuff = new byte[1024];
                int recvBytes = socket.Receive(recvBuff); // 몇 byte를 받았는지 int로 반환
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes); // 어디에서, 어디부터, 얼만큼 받아올 것인지를 매개변수로 입력
                Console.WriteLine($"[From Server] {recvData}");

                // 나간다
                socket.Shutdown(SocketShutdown.Both); // 연결 해제를 미리 예고
                socket.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}

 

# Listener

- 위의 코드에서 Accept() 는 Blocking 함수이므로 Client가 입장하지 않을 경우 무한히 대기한다.

 ~> Accept() 뿐만이 아니라 Send(), Receive() 역시 Blocking 방식으로 구현 되었다.

 ~> 무수한 Client를 받기 위해 Blocking 함수를 채택하는 것은 바람직하지 않을 수 있다.

 ~> 이를 NonBlocking 방식으로 구현하여 해결할 수 있다.

- Blocking 과 NonBlocking 의 차이

 ~> 낚시로 비유를 하자면 Blocking 방식은 낚시대를 던져놓고, 물고기가 잡힐때까지 아무 것도 하지 않으면서 낚시대만

      뜷어져라 쳐다보고 있는 것이다.

 ~> 그러나 NonBlocking 방식은 낚시대를 던져놓고, 던지자마자 입질이 올 경우 바로 낚시대를 끌어 올리지만

      던진 그 순간에 바로 입질이 오지 않을 경우 본인이 할 다른 일들을 하고 있다가 입질이 올 경우 그제서야 돌아와

      낚시대를 끌어올리는 것이다.

Accept를 NonBlocking 방식으로 구현하기 위한 Listener Class 생성
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    internal class Listener
    {
        Socket _listenSocket;
        Action<Socket> _onAcceptHandler;

        public void Init(IPEndPoint endPoint, Action<Socket> onAcceptHandler) // Socket 함수의 매개변수를 위한 endPoint, _onAcceptHandler의 event 함수로 등록하기 위한 onAcceptHandler (delegate를 통해 함수를 인자로 받은 것)
        {
            // 문지기 고용
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _onAcceptHandler += onAcceptHandler; // onAcceptHandler 함수가 _onAcceptHandler event를 구독 신청

            // 문지기 교육
            _listenSocket.Bind(endPoint);

            // 영업 시작
            // backlog : 최대 대기 수
            _listenSocket.Listen(10); // backlog를 매개변수로 입력

            // SocketAsyncEventArgs 는 일꾼 같은 친구임 ( Async 특징상 우리가 당장 값을 추출할 수 없었으니 이런 저런 정보들을 이 친구를 통해 전달받는 것 )
            SocketAsyncEventArgs args = new SocketAsyncEventArgs(); // 한번만 만들어주면 계속해서 재사용이 가능하다는 장점을 가진다. ( args 라는 event를 생성 )

            // AcceptAsync() 가 완료된 경우 우리에게 call back 방식으로 연락을 줄 수 있도록 event 사용 (딴 일을 하고 있다가 입질이 올 경우 args가 OnAcceptCompleted 함수를 자동으로 호출)
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted); // OnAcceptCompleted 메소드가 args Completed event를 구독 신청

            // *** 낚시대를 던진다 ***
            RegisterAccept(args); // 최초 1번은 직접 등록해준다.
        }

        void RegisterAccept(SocketAsyncEventArgs args) // AcceptAsync 함수의 매개변수를 위한 args
        {
            args.AcceptSocket = null; // 초기화 하지 않을 경우 Crash 발생

            bool pending = _listenSocket.AcceptAsync(args); // 비동기 Accept() 함수

            // *** 낚시대를 던지자마자 물고기가 잡힌 경우 ***
            if (pending == false) // AcceptAsync를 실행하는 동시에 Client로부터 접속 요청이 와서 pending 없이 완료된 경우
                OnAcceptCompleted(null, args); // 직접 OnAcceptCompleted 함수를 호출

            // 만약 pending이 true인 경우 당장은 완료되지 않았지만 나중에라도 완료가 된다면 위의 args가 자동으로 본인을 구독 신청한 구독자들에게 알려준다. (OnAcceptCompleted 함수를 호출)
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args) // call back 함수로 등록하기 위한 매개변수 sender와 args ( 즉, 형식을 맞춰주기 위한 것 )
        {
            // *** 낚은 물고기를 빼낸다 ***
            if (args.SocketError == SocketError.Success) // Client로부터 접속 요청이 와서 Accept까지 성공적으로 마친 경우
            {
                _onAcceptHandler.Invoke(args.AcceptSocket); // _onAcceptHandler가 본인을 구독 신청한 구독자들에게 알려준다. (onAcceptHandler 함수를 호출) (args.AcceptSocket 는 ClientSocket을 뱉어준다.)
            }
            else
                Console.WriteLine(args.SocketError.ToString());

            // *** 다시 낚시대를 던진다 ***
            RegisterAccept(args); // 여기까지 도달한 경우 다음 Client를 위해서 다시 등록해주는 것
        }
    }
}​

 

Accept를 NonBlocking 방식으로 구현하기 위한 Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();

        static void OnAcceptHandler(Socket clientSocket) // Client Accept가 완료된 경우
        {
            try
            {
                // 받는다
                byte[] recvBuff = new byte[1024];
                int recvBytes = clientSocket.Receive(recvBuff); // 몇 byte를 받았는지 int로 반환
                string recvData = Encoding.UTF8.GetString(recvBuff, 0, recvBytes); // 어디에서, 어디부터, 얼만큼 받아올 것인지를 매개변수로 입력
                Console.WriteLine($"[From Client] {recvData}");

                // 보낸다
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                clientSocket.Send(sendBuff);

                // 쫓아낸다
                clientSocket.Shutdown(SocketShutdown.Both); // 미리 예고
                clientSocket.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        static void Main(string[] args)
        {
            // DNS (Domain Name System) : Domain을 IP 네트워크에서 찾아갈 수 있는 IP로 변환해 준다. 
            string host = Dns.GetHostName(); // Local Computer의 host 이름을 반환
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; // ip 주소를 배열로 반환 (예를 들어 Google과 같이 Traffic이 어마무시한 사이트는 여러개의 ip 주소를 가질 수 있기 때문)
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip 주소와 port 번호를 매개변수로 입력
			
            // 손님을 입장시킨다.
            _listener.Init(endPoint, OnAcceptHandler); // 문지기에게 endPoint 전달, 혹시라도 누군가 접속 요청 후 Accept 완료시 call back을 받기 위해 OnAcceptHandler를 event 함수로 등록하고자 전달
            Console.WriteLine("Listening...");

            while (true)
            {
                
            }
        }
    }
}

 

+ 추가 정리

 

# Session #1

Receive를 NonBlocking 방식으로 구현하기 위한 Session Class 생성
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    class Session
    {
        Socket _socket;
        int _disconnected = 0; // 끊김의 여부를 판단할 flag (MultiThread 환경에서 Disconnect() 를 2번 이상 호출하는 것을 방지)

        public void Start(Socket socket)
        {
            _socket = socket;
            SocketAsyncEventArgs recvArgs = new SocketAsyncEventArgs();

            // ReceiveAsync() 가 완료된 경우 우리에게 call back 방식으로 연락을 줄 수 있도록 event 사용 (딴 일을 하고 있다가 데이터가 올 경우 recvArgs가 OnRecvCompleted 함수를 자동으로 호출)
            recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);

            // 데이터를 받기 위한 Buffer 설정
            recvArgs.SetBuffer(new byte[1024], 0, 1024);

            RegisterRecv(recvArgs);
        }

        public void Send(byte[] sendBuff)
        {
            _socket.Send(sendBuff);
        }

        public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
                return;
            
            _socket.Shutdown(SocketShutdown.Both); // 미리 예고
            _socket.Close();
        }


        void RegisterRecv(SocketAsyncEventArgs args)
        {
            bool pending = _socket.ReceiveAsync(args); // 비동기 Receive() 함수

            if (pending == false) // ReceiveAsync를 실행하는 동시에 받고자 하는 데이터가 존재하여 pending 없이 완료된 경우
                OnRecvCompleted(null, args); // 직접 OnRecvCompleted 함수를 호출
        }

        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 데이터를 성공적으로 받은 경우
            {
                try
                {
                    string recvData = Encoding.UTF8.GetString(args.Buffer, args.Offset, args.BytesTransferred);
                    Console.WriteLine($"[From Client] {recvData}");

                    RegisterRecv(args);
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed {e}");
                }
            }
            else
            {
                Disconnect();
            }
        }
    }
}​

 

Receive를 NonBlocking 방식으로 구현하기 위한 Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    class Program
    {
        static Listener _listener = new Listener();

        static void OnAcceptHandler(Socket clientSocket) // Client Accept가 완료된 경우
        {
            try
            {
                // 받는다.
                Session session = new Session(); // Session Class 객체 생성
                session.Start(clientSocket);

                // 보낸다.
                byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
                session.Send(sendBuff);

                // 쫓아낸다.
                session.Disconnect();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }

        static void Main(string[] args)
        {
            // DNS (Domain Name System) : Domain을 IP 네트워크에서 찾아갈 수 있는 IP로 변환해 준다. 
            string host = Dns.GetHostName(); // Local Computer의 host 이름을 반환
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; // ip 주소를 배열로 반환 (예를 들어 Google과 같이 Traffic이 어마무시한 사이트는 여러개의 ip 주소를 가질 수 있기 때문)
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777); // ip 주소와 port 번호를 매개변수로 입력

            // 손님을 입장시킨다.
            _listener.Init(endPoint, OnAcceptHandler); // 문지기에게 endPoint 전달, 혹시라도 누군가 접속 요청 후 Accept 완료시 call back을 받기 위해 OnAcceptHandler를 event 함수로 등록하고자 전달
            Console.WriteLine("Listening...");

            while (true)
            {
                
            }
        }
    }
}​

 

# Session #2

- Send는 시점이 정해져 있지 않다.

 ~> Send시 보내줄 Buffer와 Msg를 같이 설정해줘야 하는데 미래에 어떤 Msg를 보낼지 모르기 때문에

      Receive와 같은 방식으로 접근할 수 없다.

- 또한 Send는 OnSendCompleted() 호출 후 RegisterSend(sendArgs) 를 통해 다시 등록하는 것이 불가능하다.

 ~> 이는 똑같은 정보를 다시 보내는 것이기 때문이며, 따라서 재사용이 불가능하다.

- 마지막으로 MultiThread 환경에서 동시다발적으로 Send() 를 호출시 매번 RegisterSend() 가 호출되어 SendAsync를

  매번 호출하게 되는데 이는 네트워크 과부화를 유발한다.

 ~> 이는 운영체제가 Kernel에서 처리하기 때문이다.

- 이를 바탕으로 Send는 다음과 같이 구현할 예정이다.

 ~>  _sendArgs를 전역변수로 선언하여 재사용이 가능하도록 만들 것이다.

 ~>  SendAsync를 매번 호출하는 것이 아닌 _sendQueue에 데이터가 어느정도 모일 경우 호출할 것이다.

Send를 NonBlocking 방식으로 구현하기 위한 Session Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    class Session
    {
        // ...

        object _lock = new object();
        Queue<byte[]> _sendQueue = new Queue<byte[]>();
        bool _pending = false; // pending 여부 판단 (true인 경우 누군가 보내고 있는 것, false인 경우 아무도 보내고 있지 않은 것)
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs(); // Send는 언제 호출될지 모르기 때문에 전역 변수로 선언

        public void Start(Socket socket)
        {
            // ...

            // SendAsync() 가 완료된 경우 우리에게 call back 방식으로 연락을 줄 수 있도록 event 사용 (딴 일을 하고 있다가 데이터를 보낼 경우 _sendArgs가 OnSendCompleted 함수를 자동으로 호출)
            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

            // ...
        }

        public void Send(byte[] sendBuff)
        {
            lock (_lock)
            {
                // 누군가 RegisterSend()를 하고 있어 SendAsync() 가 끝난 뒤 OnSendCompleted() 가 완료되기 전까지는 보내지않고 sendBuff를 Queue에 넣고 종료
                // ~> 즉, 누군가 보내고 있으면 _pending 은 true이므로 본인은 보내지 않고 Queue에 집어넣기만 하는 것
                _sendQueue.Enqueue(sendBuff);
                if (_pending == false) // RegisterSend() 를 가장 먼저 호출하여 전송까지 할 수 있는 상태인 경우
                    RegisterSend();
            }
        }

        public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
                return;
            
            _socket.Shutdown(SocketShutdown.Both); // 미리 예고
            _socket.Close();
        }

        void RegisterSend()
        {
            _pending = true;
            byte[] buff = _sendQueue.Dequeue();
            _sendArgs.SetBuffer(buff, 0, buff.Length);

            bool pending = _socket.SendAsync(_sendArgs); // 비동기 Send() 함수
            if (pending == false) // SendAsync를 실행하는 동시에 받고자 하는 데이터가 존재하여 pending 없이 완료된 경우
                OnSendCompleted(null, _sendArgs);
        }

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock) // 오직 RegisterSend() 를 통해서만 OnSendCompleted() 가 호출되는 경우에는 필요 X (그러나, 우린 call back을 통해서도 호출 O)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 데이터를 성공적으로 보낸 경우
                {
                    try
                    {
                        // _pending이 false인 상황에서 A Thread가 Send()시 RegisterSend() 를 호출하게 되는데
                        // 만약 pending이 true여서 OnSendCompleted() 가 바로 호출되는 것이 아닌 call back을 통해 나중에 호출될 경우를 가정하자.
                        // 이러한 상황에서 만약 다른 Thread들이 Send()시 _pending이 true이기 때문에 RegisterSend() 를 호출하지 않고 Queue에 집어넣기만 한다.
                        // 이때 A Thread가 OnSendCompleted() 호출시 본인 데이터뿐만이 아닌 Queue에 있는 데이터들까지 처리해준다.
                        // ~> 즉, 가장 첫번째 예약 대기자가 본인의 예약을 기다리는 동안, 그 후에 예약을 건 사람들의 몫까지 처리해주는 것이다.
                        if (_sendQueue.Count > 0) // Queue에 보내야 할 데이터가 남아있는 경우
                            RegisterSend(); // 다음 전송을 위해 RegisterSend() 호출
                        else // Queue에 보내야 할 데이터가 남아있지 않은 경우
                            _pending = false; 
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }
        }


        // ...
    }
}

 

+ 추가 정리

- 어차피 OnSendCompleted()에서 _sendQueue.Count가 0보다 큰 경우 RegisterSend()를 계속해서 호출하게 되는데

  그렇게 되면 결국에는 성능적인 관점에서 달라진 것은 거의 없다고 볼 수 있다.

 ~> 100명의 사람이 Send() 를 호출시 SendAsync() 도 언젠가는 100번 호출 되기 때문이다.

 ~> 즉, 위의 코드가 완전한 해결책이라고는 보기 어렵다.

 ~> 따라서 위의 코드는 1차적으로 Send() 를 Async 계열로 바꿨다는 것에 의의를 가진다.

# Session #3

- 위의 코드에서 아쉬운 점은 _sendQueue에 있는 데이터 1개당 SendAsync() 를 1번씩 호출하고 있다는 것이다.

 ~> BufferList를 통해 해결이 가능하다.

BufferList를 통해 NonBlocking 방식의 Send를 개선하기 위한 Session Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    class Session
    {
        // ...
        // _pendingList 사용시 _pending 변수도 필요 X
        SocketAsyncEventArgs _sendArgs = new SocketAsyncEventArgs(); 
        SocketAsyncEventArgs _recvArgs = new SocketAsyncEventArgs(); // Receive도 Send와 마찬가지로 전역변수로 선언해도 무방
        List<ArraySegment<byte>> _pendingList = new List<ArraySegment<byte>>(); // 대기중인 목록들을 담기 위한 List

        // ...

        public void Send(byte[] sendBuff)
        {
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);

                // _pending 변수 대신 _pendingList.Count 사용
                if (_pendingList.Count == 0) // RegisterSend() 를 가장 먼저 호출하여 전송까지 할 수 있는 상태인 경우
                    RegisterSend();
            }
        }

        // ...

        void RegisterSend() // 해당 함수로 들어오는 시점에 _pendingList 는 언제나 null
        {
            // _sendQueue의 데이터들을 List로 연결하여 담아주게 되면 이를 SendAsync() 에 한번에 보낼 수 있다.
            // ~> 즉 _sendQueue에 있는 데이터 1개당 SendAsync() 를 1번씩 호출하는 것을 보완
            while (_sendQueue.Count > 0)
            {
                byte[] buff = _sendQueue.Dequeue();

                // _sendArgs.BufferList에 직접 Add할 경우 오류 발생
                _pendingList.Add(new ArraySegment<byte>(buff, 0, buff.Length)); // ArraySegment 는 어떤 배열의 일부를 나타내는 구조체이다.
            }
            _sendArgs.BufferList = _pendingList;         

            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
        }

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success) // 데이터들을 성공적으로 보낸 경우
                {
                    try
                    {
                        // 초기화
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        if (_sendQueue.Count > 0)
                            RegisterSend();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }
        }

        // ...
    }
}​

 

+ 추가 정리

- 위의 코드 역시 완벽하다고 할 수 없다.

 ~> RegisterSend() 에서 _sendQueue를 무조건 비워 모든 정보를 한번에 보내고 있는데, 일정 짧은 시간 동안 몇 Byte를

      보냈는지 추적하여 너무 과하게 보낸다 싶으면 쉬어 가며 보내는 것이 성능적으로 더 좋다.

 ~> 또한 상대방이 악의적으로 의미없는 정보를 담은 다수의 패킷을 보내는 경우 이를 체크하여 비정상적이다 싶으면

      받지 않도록 하는 것이 좋다.

 ~> 또한 때에 따라서는 패킷 자체를 1번에 작은 단위로 보내는 것이 아닌 뭉쳐 보내야 하는 경우도 존재한다.

 

# Session #4

- 추후를 위해 Engine과 Content를 분리하고자 한다.

 ~> event handler 를 통한 방법과 상속을 통한 방법이 존재한다.

 ~> 상속을 통한 방법이 더 간단하기 때문에 Session을 상속 받은 GameSession Class를 생성하여

      Engine과 Content를 분리해 볼 것이다.

- Content 에서는 Session을 상속받은 GameSession을 생성하여 실행하고자 하는 코드들을 추가하고,

  Engine 에서는 GameSession 내의 함수들이 무엇을 실행하는지는 잘 모르겠지만 적절한 타이밍에 해당 함수들을

  호출만 해주는 것이다.

Engine과 Content를 분리하기 위한 Session Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    abstract class Session
    {
        // ...

        // Session을 상속받은 Class에서 구현하도록 abstract로 선언
        // Engine에서 구현하는 것이 아닌 Content에서 구현하는 것이다.
        // Engine에서는 해당 함수를 호출만 한다.
        public abstract void OnConnected(EndPoint endPoint); // OnConnected() 함수는 엄밀히 말하면 Listener Class 에서 호출 (즉, Session Class 에서는 호출 X)
        public abstract void OnRecv(ArraySegment<byte> buffer);
        public abstract void OnSend(int numOfBytes);
        public abstract void OnDisconnected(EndPoint endPoint);

        // ...

        public void Disconnect()
        {
            if (Interlocked.Exchange(ref _disconnected, 1) == 1)
                return;

            OnDisconnected(_socket.RemoteEndPoint); // OnDisconnected() 함수 호출 (RemoteEndPoint는 본인과 연결된 대상)

            _socket.Shutdown(SocketShutdown.Both);
            _socket.Close();
        }

        // ...

        void OnSendCompleted(object sender, SocketAsyncEventArgs args)
        {
            lock (_lock)
            {
                if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
                {
                    try
                    {
                        _sendArgs.BufferList = null;
                        _pendingList.Clear();

                        OnSend(_sendArgs.BytesTransferred); // OnSend() 함수 호출

                        if (_sendQueue.Count > 0)
                            RegisterSend();
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnSendCompleted Failed {e}");
                    }
                }
                else
                {
                    Disconnect();
                }
            }
        }


        // ...

        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    OnRecv(new ArraySegment<byte>(args.Buffer, args.Offset, args.BytesTransferred)); // OnRecv() 함수 호출

                    RegisterRecv();
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed {e}");
                }
            }
            else
            {
                Disconnect();
            }
        }
    }
}

 

Engine과 Content를 분리하기 위한 Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using static System.Collections.Specialized.BitVector32;

namespace ServerCore
{
    class GameSession : Session // Session Class를 상속 받은 GameSession Class 생성
    {
        public override void OnConnected(EndPoint endPoint) // OnConnected() 함수 override
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            // 보낸다.
            byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
            Send(sendBuff);

            Thread.Sleep(1000);

            // 쫓아낸다.
            Disconnect();
        }

        public override void OnDisconnected(EndPoint endPoint) // OnDisconnected() 함수 override
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override void OnRecv(ArraySegment<byte> buffer) // OnRecv() 함수 override
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");
        }

        public override void OnSend(int numOfBytes) // OnSend() 함수 override
        {
            Console.WriteLine($"Transferred bytes : {numOfBytes}");
        }
    }

    class Program
    {
        static Listener _listener = new Listener();

        // 기존에는 OnAcceptHandler() 를 call back 하는 방식이였다.
        //  ~> 잘못된건 아니지만 Session Class 를 상속받은 다양한 Session 개념 등장으로 수정
        //  ~> OnAcceptHandler() 함수 내의 session.Start(clientSocket) 는 Content 보단 Engine 에 있는것이 적절

        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0]; 
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            // 손님을 입장시킨다.
            // 따라서 어떤 Session을 만들지를 Init() 매개변수로 전달하면 Engine에서 이를 처리해주는 방식으로 수정
            _listener.Init(endPoint, () => { return new GameSession(); }); // Func<Session> 을 Lambda 식으로 작성
            Console.WriteLine("Listening...");

            while (true)
            {
                
            }
        }
    }
}​

 

Engine과 Content를 분리하기 위한 Listener Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    internal class Listener
    {
        Socket _listenSocket;

        Func<Session> _sessionFactory; // Session을 어떤 방식으로, 어떤 Session 만들 것인지

        // GameSession Class 는 Content 쪽에 존재하기 때문에 
        // Listenr와 Session과 같이 Engine 쪽에서 new 를 통해 생성하지 않도록 
        // Init()의 delegate 매개변수를 통해 어떤 Session을 만들지에 대한 함수를 인자로 받은 뒤
        // 해당 함수를 _sessionFactory event 구독 신청을 통해 실행되도록 수정
        public void Init(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            // 문지기 고용
            _listenSocket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

            _sessionFactory += sessionFactory; // sessionFactory 함수가 _sessionFactory event를 구독 신청

            // 문지기 교육
            _listenSocket.Bind(endPoint);

            // 영업 시작
            // backlog : 최대 대기 수
            _listenSocket.Listen(10);

            SocketAsyncEventArgs args = new SocketAsyncEventArgs(); 
            args.Completed += new EventHandler<SocketAsyncEventArgs>(OnAcceptCompleted);

            RegisterAccept(args);
        }

        void RegisterAccept(SocketAsyncEventArgs args) 
        {
            args.AcceptSocket = null; 

            bool pending = _listenSocket.AcceptAsync(args); 
            if (pending == false) 
                OnAcceptCompleted(null, args);
        }

        void OnAcceptCompleted(object sender, SocketAsyncEventArgs args) 
        {
            if (args.SocketError == SocketError.Success) 
            {
                // 받는다.
                Session session = _sessionFactory.Invoke(); // _sessionFactory 를 통한 GameSession 생성 (Content 딴에서 요구한 방식대로 Session 생성)
                session.Start(args.AcceptSocket);
                session.OnConnected(args.AcceptSocket.RemoteEndPoint);
            }
            else
                Console.WriteLine(args.SocketError.ToString());

            RegisterAccept(args);
        }
    }
}​

 

# Connector

- 현재 DummyClient 에서 Connect() 는 Blocking 함수이다.

 ~> 게임에서 Blocking 함수 사용은 지양해야하므로 이를 NonBlocking 방식으로 구현하여 해결할 수 있다.

- 분산 Server 구현시 Server 끼리 통신하기 위해서는 한쪽은 Listener의 역할을, 한쪽은 Connector의 역할을 해야만 한다.

Connect를 NonBlocking 방식으로 구현하기 위한 Connector Class 생성
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;

namespace ServerCore
{
    class Connector
    {
        Func<Session> _sessionFactory; // Session을 어떤 방식으로, 어떤 Session 만들 것인지

        public void Connect(IPEndPoint endPoint, Func<Session> sessionFactory)
        {
            // 휴대폰 설정
            Socket socket = new Socket(endPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            _sessionFactory = sessionFactory; 

            SocketAsyncEventArgs args = new SocketAsyncEventArgs();
            args.Completed += OnConnectCompleted;
            args.RemoteEndPoint = endPoint; // 연결하기 위해 상대방의 주소를 넘겨준다.
            args.UserToken = socket; // UserToken을 통해 원하는 정보를 넘겨줄 수 있다.
            // socket을 전역변수로 선언하는 것이 아닌 UserToken을 통해 정보를 넘겨주는 이유는
            // Listener 가 계속해서 뺑뺑이를 돌며 여러명을 받을 수 있듯이
            // Connector 역시 여러명을 받을 수 있기 때문이다.

            RegisterConnect(args);
        }

        void RegisterConnect(SocketAsyncEventArgs args)
        {
            Socket socket = args.UserToken as Socket;
            if (socket == null)
                return;

            bool pending = socket.ConnectAsync(args);
            if (pending == false)
                OnConnectCompleted(null, args);
        }

        void OnConnectCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                Session session = _sessionFactory.Invoke(); // _sessionFactory 를 통한 Session 생성 (Content 딴에서 요구한 방식대로 Session 생성)
                session.Start(args.ConnectSocket);
                session.OnConnected(args.RemoteEndPoint);
            }
            else
            {
                Console.WriteLine($"OnConnectCompleted Fail: {args.SocketError}");
            }
        }
    }
}​

 

- 현재 Connector를 DummyClient 에서 사용할 수 없다. (Connector는 ServerCore의 Class)

 ~> 그렇다고 ServerCore의 Connector, Listener, Session Class를 복사하여 DummyClient 에 붙여넣는 것은 세련되지 X

- 추후에 DummyClient, Server, ServerCore 를 동시에 사용하게 된다.

 ~> 이때 ServerCore 는 실제로 실행되는 것이 아닌 라이브러리로만 사용할 예정이다.

- ServerCore를 라이브러리로 세팅한 뒤 DummyClient와 Server는 ServerCore 라이브러리를 참조하도록 설정한다.

 

- 우선 [ ServerCore 프로젝트 ] - [ 오른쪽 마우스 ] - [ 속성 ] 의 [ 애플리케이션 ] - [ 일반 ] 에서 출력 유형을

  [ 콘솔 애플리케이션 ] 이 아닌 [ 클래스 라이브러리 ] 를 선택한다.

 ~> 클래스 라이브러리 선택시 해당 프로젝트는 독립적으로 실행할 수 없다.

 ~> 즉, 다른 프로젝트에 기생하여 간접적으로 실행될 수 있다.

 

- 그 후 [ DummyClient / Server 프로젝트 ] - [ 오른쪽 마우스 ] - [ 추가 ] - [ 프로젝트 참조 ] 의 [ 프로젝트 ] 에서 ServerCore

  를 선택한다.

 ~> 이를 통해 DummyClient 와 Server 프로젝트는 ServerCore 라이브러리를 참조한다.

더이상 ServerCore의 Program Class 는 사용하지 않으므로 해당 내용을 Server의 Program Class로 복사한 뒤 삭제

(Server 입장에서 ServerCore는 다른 프로젝트이므로 ServerCore의 Class들의 보호 수준을 public으로 변경)
(이를 통해 Server는 Content, ServerCore는 Engine의 역할을 맡은 것)

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using ServerCore; // Servercore 라이브러리 참조

namespace Server
{
    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            // 보낸다.
            byte[] sendBuff = Encoding.UTF8.GetBytes("Welcome to MMORPG Server!");
            Send(sendBuff);

            Thread.Sleep(1000);

            // 쫓아낸다.
            Disconnect();
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override void OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes : {numOfBytes}");
        }
    }

    class Program
    {
        static Listener _listener = new Listener();

        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            // 손님을 입장시킨다.
            _listener.Init(endPoint, () => { return new GameSession(); });
            Console.WriteLine("Listening...");

            while (true)
            {

            }
        }
    }
}​

 

 

- [ 솔루션 ] - [ 오른쪽 마우스 ] - [ 속성 ] 의 [ 공용 속성 ] - [ 시작 프로젝트 ] 에서 [ 한 개의 시작 프로젝트 ] 가 아닌

  [ 여러 개의 시작 프로젝트 ] 를 선택한 뒤 DummyClient 와 Server 프로젝트의 작업 상태를 [ 없음 ] 에서 [ 시작 ] 으로,

  ServerCore 프로젝트의 작업 상태를 [ 시작 ] 에서 [ 없음 ] 으로 변경한다.

Connector Class 사용을 위한 Client 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using ServerCore; // Servercore 라이브러리 참조

namespace DummyClient
{
    // GameSession Class 추가
    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            // 보낸다
            byte[] sendBuff = Encoding.UTF8.GetBytes("Hello World");
            Send(sendBuff);
        }

        public override void OnDisconnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnDisconnected : {endPoint}");
        }

        public override void OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Server] {recvData}");
        }

        public override void OnSend(int numOfBytes)
        {
            Console.WriteLine($"Transferred bytes : {numOfBytes}");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            string host = Dns.GetHostName();
            IPHostEntry ipHost = Dns.GetHostEntry(host);
            IPAddress ipAddr = ipHost.AddressList[0];
            IPEndPoint endPoint = new IPEndPoint(ipAddr, 7777);

            // Connector Class 사용
            Connector connector = new Connector();
            connector.Connect(endPoint, () => { return new GameSession(); });

            try
            {
                
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }
    }
}​

 

# TCP vs UDP

- 게임에서는 패킷 단위로 통신이 이루어진다.

 

# RecvBuffer

- TCP 특성상 (일부만 보내는 흐름/혼잡제어) Client 에서 보낸 패킷이 100 Byte 라고 해서 100 Byte가 완전히 도착한다는

 보장은 없다.

 ~> 100 Byte 미만의 패킷이 도착한 경우 이를 바로 처리할 수 없기 때문에 이를 recvBuffer에 보관만 하고 있다가

      추후에 나머지 패킷이 마저 도착하면 이를 조립한 뒤 한번에 처리할 수 있도록 수정한다.

RecvBuffer 개선을 위한 RecvBuffer Class 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public class RecvBuffer
    {
        // byte 배열을 들고 있어도 되지만 byte 를 들고 있는 이유?
        // ~> 엄청 큰 Byte 배열에서 부분적으로 잘라 사용하고 싶을 수도 있기 때문
        ArraySegment<byte> _buffer;

        // _readPos와 _writePos는 Buffer에서 커서 역할을 한다.
        // Buffer의 크기는 8Byte, 1패킷이 3Byte 라고 가정했을때 아래의 예시를 살펴보자.
        // [r w] [ ] [ ] [ ] [ ] [ ] [ ] [ ]  (초기 상태)
        // [r] [ ] [ ] [w] [ ] [ ] [ ] [ ]  (Client로부터 3Byte를 받은 상태)
        // [ ] [ ] [ ] [r w] [ ] [ ] [ ] [ ]  (Content 코드에서 1패킷을 처리한 상태)
        int _readPos;
        int _writePos;

        // 생성자
        public RecvBuffer(int bufferSize)
        {
            _buffer = new ArraySegment<byte>(new byte[bufferSize], 0, bufferSize);
        }

        public int DataSize { get { return _writePos - _readPos; } }
        public int FreeSize { get { return _buffer.Count - _writePos; } }

        public ArraySegment<byte> ReadSegment // 현재까지 받은 데이터의 유효 범위가 어디서부터 어디까지인가?
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _readPos, DataSize); }
        }

        public ArraySegment<byte> WriteSegment // 다음 Recv시 빈 공간의 유효 범위가 어디서부터 어디까지인가?
        {
            get { return new ArraySegment<byte>(_buffer.Array, _buffer.Offset + _writePos, FreeSize); }
        }

        // Clean 함수를 통해 Buffer를 정리하지 않을 경우 [ ] [ ] [ ] [ ] [ ] [ ] [r] [w] 와 같이 _readPos와 _writePos가 Buffer의 끝까지 밀릴 수 있다.
        // _readPos와 _writePos의 위치가 다른 경우 _readPos 부터  _writePos - 1 까지의 데이터를 복사한 뒤 _writePos 를 이동시킨다.
        // _readPos와 _writePos의 위치가 같은 경우 데이터를 복사하지 않고 _readPos 와  _writePos 를 Buffer의 시작 위치로 이동시킨다.
        public void Clean()
        {
            int dataSize = DataSize;
            if (dataSize == 0) // _readPos와 _writePos의 위치가 같은 경우 (Client에서 보낸 모든 데이터를 처리한 상태)
            {
                _readPos = _writePos = 0;
            }
            else // _readPos와 _writePos의 위치가 다른 경우
            {
                // 어느 배열의? 어디서부터? 어느 배열의? 어디로? 얼마만큼?
                Array.Copy(_buffer.Array, _buffer.Offset + _readPos, _buffer.Array, _buffer.Offset, dataSize);
                _readPos = 0;
                _writePos = dataSize;
            }
        }

        public bool OnRead(int numOfBytes) // Content 코드에서 성공적으로 데이터를 가공한 경우 해당 함수를 통해 커서 위치를 이동시킨다.
        {
            if (numOfBytes > DataSize)
                return false;
            _readPos += numOfBytes;
            return true;
        }

        public bool OnWrite(int numOfBytes) // Client로부터 데이터를 받은 경우 해당 함수를 통해 커서 위치를 이동시킨다.
        {
            if (numOfBytes > FreeSize)
                return false;
            _writePos += numOfBytes;
            return true;
        }
    }
}​

 

RecvBuffer Class 생성에 따른 Session Class 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    public abstract class Session
    {
        // ...

        // RecvBuffer Class 사용
        RecvBuffer _recvBuffer = new RecvBuffer(1024);

        // ...

        public abstract int OnRecv(ArraySegment<byte> buffer); // 반환값을 void에서 int로 수정 (얼마만큼의 데이터를 처리했는지 반환하도록)
        
        // ...

        public void Start(Socket socket)
        {
            _socket = socket;

            _recvArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnRecvCompleted);
            
            // 아래 부분 삭제
            // _recvArgs.SetBuffer(new byte[1024], 0, 1024);

            _sendArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnSendCompleted);

            RegisterRecv();
        }

        // ...

        void RegisterRecv()
        {
            // 아래 부분 추가
            _recvBuffer.Clean();
            ArraySegment<byte> segment = _recvBuffer.WriteSegment;
            _recvArgs.SetBuffer(segment.Array, segment.Offset, segment.Count);

            bool pending = _socket.ReceiveAsync(_recvArgs);
            if (pending == false)
                OnRecvCompleted(null, _recvArgs);
        }

        void OnRecvCompleted(object sender, SocketAsyncEventArgs args)
        {
            if (args.BytesTransferred > 0 && args.SocketError == SocketError.Success)
            {
                try
                {
                    // Write 커서 이동
                    if (_recvBuffer.OnWrite(args.BytesTransferred) == false) // BytesTransferred 는 수신 받은 Byte
                    {
                        Disconnect();
                        return;
                    }

                    // Content 쪽으로 데이터를 넘겨주고 얼마나 처리했는지 받는다.
                    int processLen = OnRecv(_recvBuffer.ReadSegment);
                    if (processLen < 0 | _recvBuffer.DataSize < processLen)
                    {
                        Disconnect();
                        return;
                    }

                    // Read 커서 이동
                    if (_recvBuffer.OnRead(processLen) == false)
                    {
                        Disconnect();
                        return;
                    }
                    
                    RegisterRecv();
                }
                catch (Exception e)
                {
                    Console.WriteLine($"OnRecvCompleted Failed {e}");
                }
            }
            else
            {
                Disconnect();
            }
        }
    }
}​

 

Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using ServerCore; // Servercore 라이브러리 참조

namespace Server
{
    class GameSession : Session
    {
        // ...

        // 반환값이 void 에서 int 로 수정됨에 따른 코드 수정
        public override int OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Client] {recvData}");
            
            return buffer.Count;
        }

        // ...
    }

    // ...
}​

 

Client 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using ServerCore; // Servercore 라이브러리 참조

namespace DummyClient
{
    class GameSession : Session
    {
        // ...

        // 반환값이 void 에서 int 로 수정됨에 따른 코드 수정
        public override int OnRecv(ArraySegment<byte> buffer)
        {
            string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
            Console.WriteLine($"[From Server] {recvData}");
            
            return buffer.Count;
        }

        // ...
    }

    // ...
}​

 

# SendBuffer

- Session 마다 자신의 고유 RecvBuffer를 가진다.

 ~> Client 가 보내는 정보는 각기 다 다르기 때문에 이는 당연한 사실이다.

- 그러나 sendBuffer는 Session 마다 고유하지 않고 보내는 순간에 외부에서 만들어진다.

 ~> 이는 성능적 이슈와 관련이 있다.

 ~> 만약 100명의 User가 같은 Zone 안에 있는 경우 User 1명이 이동시 해당 User의 이동 정보를 나머지 99명에게 전부

      보내야 한다. 하지만 User 1명만 이동하는 것이 아니기 때문에 이동 패킷이 99 * 100 개가 전송되어야 한다.

- sendBuffer의 Size는 어떻게 결정해야 할까?

 ~> 만약 보내고자 하는 Class의 멤버 변수로 string 과 List 와 같이 가변적인 길이를 갖는다면 어떻게 해야 할까?

 ~> Thread 마다 자신만의 Chunk 를 크게 할당한 뒤 이를 계속해서 쪼개서 사용하도록 만든다.

SendBuffer 개선을 위한 SendBuffer Class 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ServerCore
{
    public class SendBufferHelper
    {
        // ThreadLocal : 본인의 Thread 에서만 사용 가능한 전역 변수 (Thread 끼리의 경합을 없애고자 사용)
        public static ThreadLocal<SendBuffer> CurrentBuffer = new ThreadLocal<SendBuffer>(() => { return null; });

        public static int ChunkSize { get; set; } = 4096 * 100;

        public static ArraySegment<byte> Open(int reserveSize)
        {
            if (CurrentBuffer.Value == null) // 한번도 사용하지 않은 경우
                CurrentBuffer.Value = new SendBuffer(ChunkSize); // 새로 생성

            if (CurrentBuffer.Value.FreeSize < reserveSize) // 사용한 적은 있지만 요구한 Size보다 FreeSize가 더 작은 경우
                CurrentBuffer.Value = new SendBuffer(ChunkSize); // 기존 Chunk 를 날린뒤 새로운 Chunk 로 교체

            return CurrentBuffer.Value.Open(reserveSize);
        }

        public static ArraySegment<byte> Close(int usedSize)
        {
            return CurrentBuffer.Value.Close(usedSize);
        }
    }

    public class SendBuffer
    {
        byte[] _buffer;

        // _usedSize는 Buffer에서 커서 역할을 한다.
        // Buffer의 크기는 10Byte 라고 가정했을때 아래의 예시를 살펴보자.
        // [u] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] [ ] (초기 상태)
        int _usedSize = 0;

        public int FreeSize { get { return _buffer.Length - _usedSize; } }

        // 생성자
        public SendBuffer(int chunkSize)
        {
            _buffer = new byte[chunkSize];
        }

        public ArraySegment<byte> Open(int reserveSize) // 최대 reserveSize 만큼의 Buffer 공간을 사용하고자 한다.
        {
            if (reserveSize > FreeSize)
                return null;

            return new ArraySegment<byte>(_buffer, _usedSize, reserveSize);
        }

        public ArraySegment<byte> Close(int usedSize) // 실제로 사용한 Buffer 공간을 반환하고자 한다.
        {
            ArraySegment<byte> segment = new ArraySegment<byte>(_buffer, _usedSize, usedSize);
            _usedSize += usedSize;
            return segment;
        }

        // Send는 1명이 아닌 여러명에게 보내는 경우도 있기 때문에 본인이 Send 하였다고 해서 막바로 Clean 할 수 X
        // (옮기고자 하는 곳을 다른 누군가가 아직 참조중일 수도 있기 때문)
        // 따라서 SendBuffer는 Clean 하는 것이 아닌 1회용으로 사용한다.
    }
}​

 

SendBuffer Class 생성에 따른 Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using ServerCore; // Servercore 라이브러리 참조

namespace Server
{
    class Knight
    {
        public int hp;
        public int attack;
        public string name;
        public List<int> skills = new List<int>();
    }

    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            Knight knight = new Knight() { hp = 100, attack = 10 };

            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
            
            byte[] buffer = BitConverter.GetBytes(knight.hp);
            byte[] buffer2 = BitConverter.GetBytes(knight.attack);
            
            // 어느 배열의? 어디서부터? 어느 배열의? 어디로? 얼마만큼?
            Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
            Array.Copy(buffer2, 0, openSegment.Array, buffer.Length, buffer2.Length);
            
            ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);

            Send(sendBuff);
            
            Thread.Sleep(1000);
            
            // 쫓아낸다.
            Disconnect();
        }

        // ...
    }

    // ...
}​

 

Session 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    public abstract class Session
    {
        // ...
        
        Queue<ArraySegment<byte>> _sendQueue = new Queue<ArraySegment<byte>>(); // Queue의 타입을 byte[] 에서 ArraySegment<byte> 로 수정
        
        // ...

        // 매개변수의 타입을 byte[] 에서 ArraySegment<byte> 로 수정
        public void Send(ArraySegment<byte> sendBuff)
        {
            lock (_lock)
            {
                _sendQueue.Enqueue(sendBuff);

                if (_pendingList.Count == 0)
                    RegisterSend();
            }
        }

        // ...

        void RegisterSend()
        {
            while (_sendQueue.Count > 0)
            {
                ArraySegment<byte> buff = _sendQueue.Dequeue(); // buff의 타입을 byte[] 에서 ArraySegment<byte> 로 수정
                _pendingList.Add(buff);
            }
            _sendArgs.BufferList = _pendingList;         

            bool pending = _socket.SendAsync(_sendArgs);
            if (pending == false)
                OnSendCompleted(null, _sendArgs);
        }

        // ...
    }
}​

 

# PacketSession

- 게임에서는 패킷 단위로 통신이 이루어진다.

 ~> 패킷 전용 OnRecv 를 만들 필요가 있다.

Session 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ServerCore
{
    // abstract class PacketSession 추가
    public abstract class PacketSession : Session
    {
        public static readonly int HeaderSize = 2;

        // 패킷을 받은 경우 [ size (2Byte) ] [ packetId (2Byte) ] [ ... ]
        public sealed override int OnRecv(ArraySegment<byte> buffer) // sealed는 다른 Class가 PacketSession을 상속 받은 뒤 OnRecv를 override 하고자 할 경우 오류 발생
        {
            int processLen = 0; // 내가 몇 Byte 를 처리했는가

            while (true) // 패킷을 처리할 수 있을때까지 계속해서 반복
            {
                // 최소한 Header 는 Parsing 할 수 있는지 확인
                if (buffer.Count < HeaderSize) 
                    break;

                // 패킷이 완전체로 도착했는지 확인
                ushort dataSize = BitConverter.ToUInt16(buffer.Array, buffer.Offset); // ToUInt16은 Byte 배열을 ushort로 뽑아달라는 것
                if (buffer.Count < dataSize)
                    break;

                // 여기까지 왔으면 패킷 조립 가능
                OnRecvPacket(new ArraySegment<byte>(buffer.Array, buffer.Offset, dataSize)); // 패킷의 유효 범위를 넘겨준다.

                processLen += dataSize;
                buffer = new ArraySegment<byte>(buffer.Array, buffer.Offset + dataSize, buffer.Count - dataSize);
            }

            return processLen;
        }

        public abstract void OnRecvPacket(ArraySegment<byte> buffer);
    }

    // ...
}​

 

Server 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using ServerCore;

namespace Server
{
    class Packet // 패킷 설계시 최대한 Size를 압축하여 보내는 것이 중요하다.
    {
        // Packet이 완전체로 왔는지? 잘려서 왔는지? 구분할 수 있어야 한다.
        public ushort size; // ushort는 2Byte
        public ushort packetId; // ushort는 2Byte
    }

    class GameSession : PacketSession // Session 이 아닌 PacketSession 을 상속 받도록 수정
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            Packet packet = new Packet() { size = 100, packetId = 10 };

            ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
            byte[] buffer = BitConverter.GetBytes(packet.size);
            byte[] buffer2 = BitConverter.GetBytes(packet.packetId);
            // 어느 배열의? 어디서부터? 어느 배열의? 어디로? 얼마만큼?
            Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
            Array.Copy(buffer2, 0, openSegment.Array, buffer.Length, buffer2.Length);
            ArraySegment<byte> sendBuff = SendBufferHelper.Close(buffer.Length + buffer2.Length);

            Send(sendBuff);

            Thread.Sleep(1000);

            // 쫓아낸다.
            Disconnect();
        }

        // abstract 함수인 OnRecvPacket override 는 필수
        public override void OnRecvPacket(ArraySegment<byte> buffer)
        {
            ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset); // ToUInt16은 Byte 배열을 ushort로 뽑아달라는 것
            ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
            Console.WriteLine($"RecvPacketID: {id}, Size: {size}");
        }

        // Sealed 로 OnRecv 함수를 override 하는 것을 금지 시켰기 때문에 아래 부분 삭제
        //public override int OnRecv(ArraySegment<byte> buffer)
        //{
        //    string recvData = Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count);
        //    Console.WriteLine($"[From Client] {recvData}");
        //    return buffer.Count;
        //}

        // ...
    }

    // ...
}​

 

Client 코드 수정
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using ServerCore; // Servercore 라이브러리 참조

namespace DummyClient
{
    class Packet
    {
        public ushort size; // ushort는 2Byte
        public ushort packetId; // ushort는 2Byte
    }

    class GameSession : Session
    {
        public override void OnConnected(EndPoint endPoint)
        {
            Console.WriteLine($"OnConnected : {endPoint}");

            Packet packet = new Packet() { size = 4, packetId = 7 };

            for (int i = 0; i < 5; i++)
            {
                ArraySegment<byte> openSegment = SendBufferHelper.Open(4096);
                byte[] buffer = BitConverter.GetBytes(packet.size);
                byte[] buffer2 = BitConverter.GetBytes(packet.packetId);
                // 어느 배열의? 어디서부터? 어느 배열의? 어디로? 얼마만큼?
                Array.Copy(buffer, 0, openSegment.Array, openSegment.Offset, buffer.Length);
                Array.Copy(buffer2, 0, openSegment.Array, buffer.Length, buffer2.Length);
                ArraySegment<byte> sendBuff = SendBufferHelper.Close(packet.size);

                Send(sendBuff);
            }
        }

        // ...
    }

    // ...
}​

 

# 서버 프레임워크 요약 정리

 

 

 

 

[ 섹션 1. 데이터 갖고 놀기 ]

# 2진수, 10진수, 16진수

- 10진수 : 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ...

- 2진수 : 0b0, 0b1, 0b10, 0b11, 0b100, 0b101, 0b110, 0b111, 0b1000, 0b1001 ...

- 16진수 : 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF ...

 ~> 2진수를 4개씩 끊어서 표현

- 즉, int hp = 100 (10진수),  int hp = 0b01100100 (2진수), int hp = 0x64 (16진수) 모두 같은 값을 대입하는 것이다.

 

# 정수 범위의 비밀

- 최상위 Bit는 부호 Bit에 해당하며, 대부분의 System에서는 2의 보수법을 통해 음수를 표현한다.

 

# float

- 컴퓨터는 부동 소수점 (float / double)을 최대한의 근차시 값으로 표현하기 때문에 연산 결과가 정확하지 않을 수 있다.

- float는 4Byte 수까지 표현할 수 있고, double은 8Byte 수까지 표현할 수 있다.

 ~> double이 float에 비해 더 많은 메모리를 차지하지만, 숫자를 보다 더 정밀하게 표현할 수 있다.

 ~> 실수 뒤에 f를 붙일 경우 float, 붙이지 않을 경우 double로 인식한다.

 

# 스트링 포맷

- int, float, short, double 등의 Type들은 ( )를 통해 Casting이 가능하다.

 ~> int a, short b 가 있을때 b = (short)a 를 통해 Casting 할 수 있다.

 ~> 하지만 string Type은 int a, string b 가 있을때 b = (string)a 와 같이 ( )를 통해 Casting 할 수 없다.

사용자로부터 입력을 받기 위한 방법
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            string input = Console.ReadLine(); // Enter를 누르는 순간에 변수에 저장된다.
            Console.WriteLine();
        }
    }
}​

 

사용자로부터 입력 받은 string을 int로 Casting 하는 방법
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            int number;
            string input = Console.ReadLine(); // Enter를 누르는 순간에 변수에 저장된다.
            
            number = int.Parse(input);
            Console.WriteLine();
        }
    }
}​​

 

String Format 방식
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            int hp = 80;
            int maxHp = 100;

            string msg = string.Format("당신의 HP는 {0}/{1} 입니다.", hp, maxHp);
            Console.WriteLine(msg);
        }
    }
}​​

 

String Interpolation 방식
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            int hp = 100;
            int maxHp = 100;
	
            // 엄밀히 말하면 Casting과는 거리가 멀긴 하다.
            string msg = $"당신의 HP는 {hp}/{maxHp} 입니다.";
            Console.WriteLine(msg);
        }
    }
}​

 

# 산술 연산

삼항 연산자 응용
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            int number = 25;

            bool isPair = ((number % 2 == 0) ? true : false);
        }
    }
}​​

 

# 데이터 마무리

- var Keyword를 통한 변수 선언시 Type을 직접 선언한 것처럼 컴파일러가 추론을 통해 Type 을 결정해준다.

 ~> var a = 10, var b = 4.14f, var c = "Hello", var d = true;

 ~> 그러나 가독성을 위해 Type을 직접 명시해주는 것이 좋다.


 
[ 섹션 2. 코드의 흐름 제어 ]

# switch

- switch문은 if/else문에 비해 응용 범위가 넓지 않다.

- 어떤 case문에도 해당되지 않을 경우 default문이 실행된다.

- case문에는 반드시 변수가 아닌 상수를 넣어야 한다.

switch문 응용
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            int choice = 0; // 0: 가위, 1: 바위, 2: 보, 3: 치트키
            switch (choice)
            {
                case 0:
                    Console.WriteLine("가위입니다.");
                    break;
                case 1:
                    Console.WriteLine("바위입니다.");
                    break;
                case 2:
                    Console.WriteLine("보입니다.");
                    break;
                case 3:
                    Console.WriteLine("치크티입니다.");
                    break;
                default:
                    Console.WriteLine("다 실패했습니다.");
                    break;
            }
        }
    }
}​​

 

# 가위-바위-보 게임

간단한 가위-바위-보 게임 구현 #1
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            // 0: 가위, 1: 바위, 2: 보

            Random rand = new Random();
            int aiChoice = rand.Next(0, 3); // 0 ~ 2 사이의 랜덤 값
            int choice = Convert.ToInt32(Console.ReadLine()); // 사용자로부터 입력 받은 값을 숫자로 변형하여 대입
            
            switch (choice)
            {
                case 0:
                    Console.WriteLine("당신의 선택은 가위입니다.");
                    break;
                case 1:
                    Console.WriteLine("당신의 선택은 바위입니다.");
                    break;
                case 2:
                    Console.WriteLine("당신의 선택은 보입니다.");
                    break;
            }

            switch (aiChoice)
            {
                case 0:
                    Console.WriteLine("컴퓨터의 선택은 가위입니다.");
                    break;
                case 1:
                    Console.WriteLine("컴퓨터의 선택은 바위입니다.");
                    break;
                case 2:
                    Console.WriteLine("컴퓨터의 선택은 보입니다.");
                    break;
            }

            if (choice == 0)
            {
                if (aiChoice == 0)
                    Console.WriteLine("무승부입니다.");
                else if (aiChoice == 1)
                    Console.WriteLine("패배입니다.");
                else
                    Console.WriteLine("승리입니다.");
            }
            else if (choice == 1)
            {
                if (aiChoice == 0)
                    Console.WriteLine("승리입니다.");
                else if (aiChoice == 1)
                    Console.WriteLine("무승부입니다.");
                else
                    Console.WriteLine("패배입니다.");
            }
            else
            {
                if (aiChoice == 0)
                    Console.WriteLine("패배입니다.");
                else if (aiChoice == 1)
                    Console.WriteLine("승리입니다.");
                else
                    Console.WriteLine("무승부입니다.");
            }
        }
    }
}​​

 

간단한 가위-바위-보 게임 구현 #2
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            // 0: 가위, 1: 바위, 2: 보

            Random rand = new Random();
            int aiChoice = rand.Next(0, 3); // 0 ~ 2 사이의 랜덤 값
            int choice = Convert.ToInt32(Console.ReadLine()); // 사용자로부터 입력 받은 값을 숫자로 변형하여 대입
            
            switch (choice)
            {
                case 0:
                    Console.WriteLine("당신의 선택은 가위입니다.");
                    break;
                case 1:
                    Console.WriteLine("당신의 선택은 바위입니다.");
                    break;
                case 2:
                    Console.WriteLine("당신의 선택은 보입니다.");
                    break;
            }

            switch (aiChoice)
            {
                case 0:
                    Console.WriteLine("컴퓨터의 선택은 가위입니다.");
                    break;
                case 1:
                    Console.WriteLine("컴퓨터의 선택은 바위입니다.");
                    break;
                case 2:
                    Console.WriteLine("컴퓨터의 선택은 보입니다.");
                    break;
            }

            if (choice == aiChoice)
                Console.WriteLine("무승부입니다.");
            else if (choice == 0 && aiChoice == 2)
                Console.WriteLine("승리입니다.");
            else if (choice == 1 && aiChoice == 0)
                Console.WriteLine("승리입니다.");
            else if (choice == 2 && aiChoice == 1)
                Console.WriteLine("승리입니다.");
            else
                Console.WriteLine("패배입니다.");
        }
    }
}​​

 

# 상수와 열거형

- const Keyword를 통해 상수 필드 또는 로컬 상수를 선언할 수 있다.

- enum Keyword는 열거형 상수를 표현하기 위한 것으로 이를 통해 상수 숫자들을 보다 의미있는 단어들로 표현할 수 있다.

 ~> 가독성 측면에서 도움이 된다. 

 ~> 별도의 지정이 없을 경우 첫번째 요소는 0, 두번째 요소는 1 등과 같이 1씩 증가된 값들을 할당받는다.

상수를 통한 가위-바위-보 게임 수정
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            const int ROCK = 1;
            const int PAPER = 2;
            const int SCISSORS = 0;

            Random rand = new Random();
            int aiChoice = rand.Next(0, 3); // 0 ~ 2 사이의 랜덤 값
            int choice = Convert.ToInt32(Console.ReadLine()); // 사용자로부터 입력 받은 값을 숫자로 변형하여 대입
            
            switch (choice)
            {
                case SCISSORS:
                    Console.WriteLine("당신의 선택은 가위입니다.");
                    break;
                case ROCK:
                    Console.WriteLine("당신의 선택은 바위입니다.");
                    break;
                case PAPER:
                    Console.WriteLine("당신의 선택은 보입니다.");
                    break;
            }

            switch (aiChoice)
            {
                case SCISSORS:
                    Console.WriteLine("컴퓨터의 선택은 가위입니다.");
                    break;
                case ROCK:
                    Console.WriteLine("컴퓨터의 선택은 바위입니다.");
                    break;
                case PAPER:
                    Console.WriteLine("컴퓨터의 선택은 보입니다.");
                    break;
            }

            if (choice == aiChoice)
                Console.WriteLine("무승부입니다.");
            else if (choice == SCISSORS && aiChoice == PAPER)
                Console.WriteLine("승리입니다.");
            else if (choice == ROCK && aiChoice == SCISSORS)
                Console.WriteLine("승리입니다.");
            else if (choice == PAPER && aiChoice == ROCK)
                Console.WriteLine("승리입니다.");
            else
                Console.WriteLine("패배입니다.");
        }
    }
}​​

 

열거형을 통한 가위-바위-보 게임 수정
namespace CSharpStudy
{ 
    class Program
    {
        enum Choice // 명시적으로 값을 선언하지 않을 경우 0부터 시작
        {
            Rock = 1,
            Paper = 2,
            Scissors = 0
        }

        static void Main(string[] args)
        {
            Random rand = new Random();
            int aiChoice = rand.Next(0, 3); // 0 ~ 2 사이의 랜덤 값
            int choice = Convert.ToInt32(Console.ReadLine()); // 사용자로부터 입력 받은 값을 숫자로 변형하여 대입
            
            switch (choice)
            {
                case (int)Choice.Scissors:
                    Console.WriteLine("당신의 선택은 가위입니다.");
                    break;
                case (int)Choice.Rock:
                    Console.WriteLine("당신의 선택은 바위입니다.");
                    break;
                case (int)Choice.Paper:
                    Console.WriteLine("당신의 선택은 보입니다.");
                    break;
            }

            switch (aiChoice)
            {
                case (int)Choice.Scissors:
                    Console.WriteLine("컴퓨터의 선택은 가위입니다.");
                    break;
                case (int)Choice.Rock:
                    Console.WriteLine("컴퓨터의 선택은 바위입니다.");
                    break;
                case (int)Choice.Paper:
                    Console.WriteLine("컴퓨터의 선택은 보입니다.");
                    break;
            }

            if (choice == aiChoice)
                Console.WriteLine("무승부입니다.");
            else if (choice == (int)Choice.Scissors && aiChoice == (int)Choice.Paper)
                Console.WriteLine("승리입니다.");
            else if (choice == (int)Choice.Rock && aiChoice == (int)Choice.Scissors)
                Console.WriteLine("승리입니다.");
            else if (choice == (int)Choice.Paper && aiChoice == (int)Choice.Rock)
                Console.WriteLine("승리입니다.");
            else
                Console.WriteLine("패배입니다.");
        }
    }
}​​

 

# while

do while문 응용
namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            string answer;

            // y를 입력할때까지 무한히 반복
            do
            {
                Console.WriteLine("강사님은 잘생기셨나요? (y/n) : ");
                answer = Console.ReadLine();
            } while (answer != "y");

            Console.WriteLine("정답입니다!");
        }
    }
}​​

 

# ref, out

 

- 기본적으로 C#은 메소드에 매개변수를 전달할 때 call by value (값 전달) 을 한다.

 ~> 이때 ref Keyword를 통해 명시적으로 call by reference (참조 전달)을 할 수 있다.

ref Keyword 응용
namespace CSharpStudy
{ 
    class Program
    {
        static void AddOne (ref int num)
        {
            num = num + 1;
        }

        static void Main(string[] args)
        {
            int a = 0;
            Program.AddOne(ref a); // 같은 Class 내의 함수를 호출시 Program 생략 가능
            Console.WriteLine(a);
        }
    }
}​

 

- out Keyword는 매개변수 한정자로 변수가 참조로 전달이 된다.

 ~> out Keyword를 사용한 매개변수는 함수 내부에서 반드시 값을 초기화 시켜줘야 한다.

 ~> 여러개의 값을 반환해야 하는 경우에 유용하다.

out Keyword 응용
namespace CSharpStudy
{ 
    class Program
    {
        // result1는 몫, result2는 나머지
        static void Divide(int a, int b, out int result1, out int result2)
        {
            result1 = a / b;
            result2 = a % b;
        }

        static void Main(string[] args)
        {
            int num1 = 10;
            int num2 = 3;

            int result1;
            int result2;
            Divide(10, 3, out result1, out result2);

            Console.WriteLine(result1);
            Console.WriteLine(result2);
        }
    }
}​

 

# 오버로딩

- 오버로딩의 사전적 의미는 "과적하다" 이며, 간단하게 말하면 함수 이름의 재사용이다.

 ~> 오버로딩시 하나의 메소드에 여러개의 구현을 과적할 수 있다.

 ~> 오버로딩은 같은 메소드 이름으로 매개변수의 개수, Type을 다르게 정의할 수 있다.

 ~> 반환 값은 오버로딩에 영향을 주지 않는다. (즉, 반환값만 달리 할 경우 오버로딩 X)

오버로딩 응용
namespace CSharpStudy
{ 
    class Program
    {
        // 함수 이름의 재사용
        static int Add(int a, int b)
        {
            return a + b;
        }

        static int Add(int a, int b, int c)
        {
            return a + b + c;
        }

        static float Add(float a, float b)
        {
            return a + b;
        }

        static void Main(string[] args)
        {
            int result = Program.Add(2, 3);
            float result2 = Program.Add(2.0f, 3.0f);
        }
    }
}​

 

- 선택적 매개변수란 매개 변수를 필수 또는 선택 사항으로 지정할 수 있다.

선택적 매개변수 응용 #1
namespace CSharpStudy
{ 
    class Program
    {
        // 선택적 매개변수
        static int Add(int a, int b, int c = 0)
        {
            return a + b + c;
        }

        static void Main(string[] args)
        {
            int result = Program.Add(1, 2);
            int result2 = Program.Add(1, 2, 3);
        }
    }
}​

 

선택적 매개변수 응용 #2
namespace CSharpStudy
{ 
    class Program
    {
        // 선택적 매개변수
        static int Add(int a, int b, int c = 0, float d = 1.0f, double e = 3.0)
        {
            return a + b + c;
        }

        static void Main(string[] args)
        {
            int result = Program.Add(1, 2, d: 2.0f);
        }
    }
}​

 


 
[ 섹션 3. TextRPG ]

# 디버깅 기초

1. 우선 변수의 값, 메모리 동작 또는 코드 분기의 실행 여부를 확인하기 위해 실행 중인 코드를 일시 중단해야 하는 위치에 중단점을 설정한다.

2. F5를 통해 디버깅을 시작할 수 있고, 디버깅이 시작된 뒤 노란색 화살표는 일시 중지된 문을 가리킨다.

3. F10을 통해 프로시저 단위로 실행할 수 있고, F11을 통해 코드를 한 단계씩 실행할 수도 있다.

 ~> 프로시저는 메소드 (함수) 와 같다. (즉, 어떤 함수를 만나더라도 해당 함수로 들어가지 말고 결과만 보겠다는 것) 

디버깅시 호출 스택을 통해 경로를 파악할 수 있다.

중단점에 [ 오른쪽 마우스 ] - [ 조건 ] 을 통해 조건을 설정할 수 있다.

디버깅시 중단점 위의 노란색 화살표를 드래그하여 실행 순서를 마음대로 조절할 수 있다.

 

# TextRPG 직업 고르기

TextRPG 직업 고르기
namespace CSharpStudy
{ 
    class Program
    {
        enum ClassType
        {
            None = 0,
            Knight = 1,
            Archer = 2,
            Mage = 3
        }

        static ClassType ChooseClass()
        {
            Console.WriteLine("직업을 선택하세요!");
            Console.WriteLine("[1] 기사");
            Console.WriteLine("[2] 궁수");
            Console.WriteLine("[3] 법사");

            ClassType choice = ClassType.None;
            string input = Console.ReadLine();
            switch (input)
            {
                case "1":
                    choice = ClassType.Knight;
                    break;
                case "2":
                    choice = ClassType.Archer;
                    break;
                case "3":
                    choice = ClassType.Mage;
                    break;
            }

            return choice;
        }

        static void Main(string[] args)
        {
            while (true)
            {
                ClassType choice = ChooseClass();
                if (choice != ClassType.None)
                    break;
            }
        }
    }
}​

 

# TextRPG 플레이어 생성 (구조체)

- 구조체는 하나 이상의 변수들을 묶어서 그룹으로 만드는 사용자 정의 자료형이다.

 

+ 추가 검색 ( https://usingsystem.tistory.com/6 )

 - Class와 구조체의 차이점

 1. Class는 Heap 영역에 할당되지만, 구조체는 Stack 영역에 할당된다.

 2. Class는 참조 타입이지만, 구조체는 값 타입이다.

 3. Class는 상속이 가능하지만, 구조체는 상속이 불가능하다.

 

TextRPG 플레이어 생성 (구조체 응용)
namespace CSharpStudy
{ 
    class Program
    {
        // ...

        struct Player
        {
            public int hp;
            public int attack;
        }

        // ...

        static void CreatePlayer(ClassType choice, out Player player)
        {
            switch (choice)
            {
                case ClassType.Knight:
                    player.hp = 100;
                    player.attack = 10;
                    break;
                case ClassType.Archer:
                    player.hp = 75;
                    player.attack = 12;
                    break;
                case ClassType.Mage:
                    player.hp = 50;
                    player.attack = 15;
                    break;
                default:
                    player.hp = 0;
                    player.attack = 0;
                    break;
            }
        }

        static void Main(string[] args) 
        {
            while (true)
            {
                ClassType choice = ChooseClass();
                if (choice != ClassType.None)
                {
                    // 캐릭터 생성
                    Player player;
                    CreatePlayer(choice, out player);
                }
            }
        }
    }
}​

 

# TextRPG 몬스터 생성

TextRPG 몬스터 생성
using System.Diagnostics.Tracing;

namespace CSharpStudy
{ 
    class Program
    {
        // ...

        enum MonsterType
        {
            None = 0,
            Slime = 1,
            Orc = 2,
            Skeleton = 3
        }

        struct Monster
        {
            public int hp;
            public int attack;
        }

        // ...

        static void CreateRandomMonster(out Monster monster)
        {
            Random rand = new Random();
            int randMonster = rand.Next(1, 4);
            switch (randMonster)
            {
                case (int)MonsterType.Slime:
                    Console.WriteLine("슬라임이 스폰되었습니다!");
                    monster.hp = 20;
                    monster.attack = 2;
                    break;
                case (int)MonsterType.Orc:
                    Console.WriteLine("오크가 스폰되었습니다!");
                    monster.hp = 40;
                    monster.attack = 4;
                    break;
                case (int)MonsterType.Skeleton:
                    Console.WriteLine("스켈레톤이 스폰되었습니다!");
                    monster.hp = 30;
                    monster.attack = 3;
                    break;
                default:
                    monster.hp = 0;
                    monster.attack = 0;
                    break;
            }
        }

        static void EnterField()
        {
            Console.WriteLine("필드에 접속했습니다!");

            // 랜덤으로 1~3 몬스터 중 하나를 스폰
            Monster monster;
            CreateRandomMonster(out monster);

            Console.WriteLine("[1] 전투 모드로 돌입");
            Console.WriteLine("[2] 일정 확률로 마을로 도망");
        }

        // ...
        static void Main(string[] args) 
        {
            while (true)
            {
                ClassType choice = ChooseClass();
                if (choice != ClassType.None)
                {
                    // 캐릭터 생성
                    Player player;
                    CreatePlayer(choice, out player);

                    // 게임 입장
                    EnterGame();
                }
            }
        }
    }
}​

 

# TextRPG 전투

TextRPG 전투
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{ 
    class Program
    {
        // ...

        static void Fight(ref Player player, ref Monster monster)
        {
            while (true)
            {
                // player가 monster 공격
                monster.hp -= player.attack;
                if (monster.hp <= 0)
                {
                    Console.WriteLine("승리했습니다!");
                    Console.WriteLine($"남은 체력 : {player.hp}");
                    break;
                }
                 
                // monster의 반격
                player.hp -= monster.attack;
                if (player.hp <= 0)
                {
                    Console.WriteLine("패배했습니다!");
                    break;
                }
            }
        }

        static void EnterField(ref Player player)
        {
            while (true)
            {
                Console.WriteLine("필드에 접속했습니다!");

                // 랜덤으로 1~3 몬스터 중 하나를 스폰
                Monster monster;
                CreateRandomMonster(out monster);

                Console.WriteLine("[1] 전투 모드로 돌입");
                Console.WriteLine("[2] 일정 확률로 마을로 도망");

                string input = Console.ReadLine();
                if (input == "1")
                    Fight(ref player, ref monster);
                else if (input == "2")
                {
                    // 도망칠 확률이 33%
                    Random rand = new Random();
                    int randValue = rand.Next(0, 101);
                    if (randValue <= 33)
                    {
                        Console.WriteLine("도망치는데 성공했습니다!");
                    }
                    else
                        Fight(ref player, ref monster);
                }
            }
        }

        static void EnterGame(ref Player player)
        {
            while (true)
            {
                Console.WriteLine("마을에 접속했습니다!");
                Console.WriteLine("[1] 필드로 간다");
                Console.WriteLine("[2] 로비로 돌아가기");

                string input = Console.ReadLine();
                if (input == "1")
                    EnterField(ref player);
                else if (input == "2")
                    break;
            }  
        }

        static void Main(string[] args) 
        {
            while (true)
            {
                ClassType choice = ChooseClass();
                if (choice == ClassType.None)
                    continue;

                // 캐릭터 생성
                Player player;
                CreatePlayer(choice, out player);

                // 게임 입장
                EnterGame(ref player);
            }
        }
    }
}​

 
[ 섹션 4. 객체지향 여행 ]

# 객체지향의 시작

- 객체 지향 프로그래밍 (Object Oriented Programming) 을 OOP 라고 한다.

- Object (객체) : Obejct는 설계도에 해당하는 Class를 통해 생성된 실체화된 것이다. (사람, 사물 등)

- Class (클래스) : Class는 Object를 만들기 위한 설계도와 같다.

 ~> 해당 객체가 어떤 속성과 기능을 가지는지 정의하는 역할을 한다.

- Instance (인스턴스) : Instance는 Class를 통해 Obejct를 생성한 결과물과 같다.

 ~> Class를 통해 Object를 생성하는 것을 인스턴스화 한다고 한다.

Class를 통해 Object를 인스턴스화 하기 위한 예제
namespace CSharpStudy
{
    class Knight
    {
        public int hp;
        public int attack;

        public void Move()
        {
            Console.WriteLine("Knight Move");
        }

        public void Attack()
        {
            Console.WriteLine("Knight Attack");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();

            knight.hp = 100;
            knight.attack = 10;
            knight.Move();
            knight.Attack();
        }
    }
}​

 

# 복사(값)와 참조

Class와 Struct의 가장 큰 차이점
namespace CSharpStudy
{
    class Knight
    {
        public int hp;
        public int attack;
    }

    struct Mage
    {
        public int hp;
        public int attack;
    }

    class Program
    {
        static void KillKnight(Knight night)
        {
            night.hp = 0;
        }

        static void KillMage(Mage mage)
        {
            mage.hp = 0;
        }

        static void Main(string[] args)
        {
            // struct는 복사
            Mage mage; // struct는 = new Mage() 생략이 가능
            mage.hp = 100;
            mage.attack = 50;
            KillMage(mage); // 함수 결과로 mage의 hp는 그대로 100

            Mage mage2 = mage;
            mage2.hp = 0; // 함수 결과로 mage의 hp는 그대로 100

            // class는 참조
            Knight knight = new Knight();
            knight.hp = 100;
            knight.attack = 10;
            KillKnight(knight); // 함수 결과로 knight의 hp는 0

            Knight knight2 = knight;
            knight2.hp = 100; // 함수 결과로 knight의 hp는 100
        }
    }
}​

 

얕은 복사와 깊은 복사의 차이
namespace CSharpStudy
{
    class Knight
    {
        public int hp;
        public int attack;

        public Knight Clone()
        {
            Knight knight = new Knight();
            knight.hp = hp;
            knight.attack = attack;
            return knight;
        }
    }

    class Program
    {
        static void KillKnight(Knight night)
        {
            night.hp = 0;
        }

        static void Main(string[] args)
        {
            // 얕은 복사
            Knight knight = new Knight();
            knight.hp = 100;
            knight.attack = 10;

            Knight knight2 = knight;
            knight2.hp = 0; // 함수 결과로 knight의 hp는 0

            // 깊은 복사
            Knight knight3 = knight.Clone();
            knight2.hp = 100; // 함수 결과로 knight의 hp는 그대로 0
        }
    }
}​

 

# 스택과 힙

- Stack과 Heap은 데이터를 위한 Memory 라는 공통점을 가지지만, 용도에 따라 구분된다.

- Stack은 메소드의 실행, 해당 메소드로 전달되는 매개변수, 메소드 내에서 사용되는 지역변수를 처리한다.

 ~> Stack은 불완전하고 일시적으로 사용하는 Memory로 메모장과 같은 존재다.

 ~> Stack 영역은 이름에서 알 수 있듯 자료구조에서 다루는 Stack과 동작방식이 같다. (LIFO)

 ~> Stack 영역은 메소드의 실행이 끝날 경우 해당 메소드와 관련된 영역은 해제된다.

 ~> new 연산자를 사용하여 객체 생성시 실제 데이터 자체는 Heap 영역에, 데이터 주소는 Stack 영역에 저장된다.

 ~> Stack 영역은 값 형식이다.

- Heap은 동적으로 할당되는 데이터와 객체들을 처리한다. (주로 Class)

 ~> Heap 영역은 new 연산자를 사용하여 객체를 생성하거나 메모리를 동적으로 할당시 사용된다.

 ~> C#은 C++과 달리 GC가 Heap 영역을 관리하여 Memory 누수 및 기타 문제를 방지한다.

      (C++은 프로그래머가 직접 delete를 통해 해제를 해야지만 Memory 누수 및 기타 문제를 방지 가능)

 ~> Heap 영역은 참조 형식이다.

 

# 생성자

- 생성자는 반환 형식을 선언하지 않는다.

- this() 생성자는 생성자에서 본인의 다른 생성자를 호출한다.

- 매개변수를 받는 생성자를 생성시, 기본 생성자를 따로 만들어주지 않는 이상 기본 생성자는 사용이 불가능하다.

생성자 응용 및 this() 생성자 응용
namespace CSharpStudy
{
    class Knight
    {
        public int hp;
        public int attack;
        public int defense;

        public Knight()
        {
            hp = 100;
            attack = 10;
            defense = 10;
        }

        public Knight(int hp)
        {
            this.hp = hp;
        }

        public Knight(int hp, int attack) : this(hp)
        {
            this.attack = attack;
        }

        public Knight(int hp, int attack, int defense) : this(hp, attack)
        {
            this.defense = defense;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();
        }
    }
}

 

# static의 정체

- Class 내부의 필드 값은 각 Instance 마다 제각각일 수 있다. (즉, 인스턴스마다 고유한 값을 가질 수 있다.)

 ~> Class를 통해 Instance가 생성될 때 각 객체마다 따로 생긴다.

- Class 내부에 static과 함께 선언한 필드와 메소드는 각 Instance에 종속되는 것이 아닌 해당 Class에 종속된다.

 ~> 해당 Class가 처음 사용될 때 한번 초기화 되며, 그 뒤로는 계속해서 동일한 Memory를 사용한다.

 ~> Class 내부에 static과 함께 선언한 필드와 메소드 사용시 Class이름.필드/메소드이름 과 같이 접근해야한다.

 ~> static을 통해 Class로 부터 Instance를 생성하지 않고 필드와 메소드를 호출할 수 있다.

- Class 내부에 static과 함께 선언한 메소드 내부에서는 static 필드만 사용 가능하다. (Class의 Instance 필드 참조 불가능)

 ~> 다만 내부에서 새로운 객체 생성시 해당 객체의 필드 참조는 가능하다.

- static과 함께 선언한 Class는 모든 멤버가 static 필드, static 메소드로 이루어져 있으며, 객체를 생성할 수 없다.

static 응용
namespace CSharpStudy
{
    class Knight
    {
        // 정적 필드
        static public int counter = 1;

        public int id;
        public int hp;
        public int attack;

        // 정적 메소드
        static public void AddCounter()
        {
            counter++;
        }

        // 정적 메소드 (static이라고 해서 Instance에 접근을 못하는 것은 X)
        static public Knight CreateKnight()
        {
            Knight knight = new Knight(); // 다만 내부에서 새로운 객체를 생성해야 한다.
            knight.hp = 100;
            knight.attack = 5;
            return knight;
        }

        public Knight()
        {
            id = counter++;
            hp = 100;
            attack = 10;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = Knight.CreateKnight();
        }
    }
}​

 

# 상속성

- 상속성은 OOP 특징 중 하나이다.

- 부모 Class (기반 Class) 로부터 상속하여 새로운 자식 Class (파생 Class)를 만들 수 있다.

- 상속을 통해 부모 Class의 필드 및 메소드들을 자식 Class에서 사용할 수 있다.

상속 응용
namespace CSharpStudy
{
    class Player // 부모 Class 또는 기반 Class
    {
        static public int counter = 1;

        public int id;
        public int hp;
        public int attack;

        public void Move()
        {
            Console.WriteLine("Player Move!");
        }

        public void Attack()
        {
            Console.WriteLine("Player Attack!");
        }
    }

    class Knight : Player // 자식 Class 또는 파생 Class
    {
        public void Stun()
        {
            Console.WriteLine("Stun!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();
            knight.Move();
            knight.Attack();
        }
    }
}​

 

상속시 생성자 응용 #1 (자식클래스가 생성될때 부모 Class의 생성자가 먼저 호출)
namespace CSharpStudy
{
    class Player // 부모 Class 또는 기반 Class
    {
        static public int counter = 1;

        public int id;
        public int hp;
        public int attack;

        public Player()
        {
            Console.WriteLine("Player 생성자 호출!");
        }
    }

    class Knight : Player // 자식 Class 또는 파생 Class
    {
        public Knight()
        {
            Console.WriteLine("Knight 생성자 호출!");
        }

        static public Knight CreateKnight()
        {
            Knight knight = new Knight();
            knight.hp = 100;
            knight.attack = 5;
            return knight;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = Knight.CreateKnight();
        }
    }
}​

 

상속시 생성자 응용 #2 (부모의 생성자가 여러개 존재할 경우 부모의 기본 생성자를 호출)
namespace CSharpStudy
{
    class Player // 부모 Class 또는 기반 Class
    {
        static public int counter = 1;

        public int id;
        public int hp;
        public int attack;

        public Player()
        {
            Console.WriteLine("Player 생성자 호출!");
        }

        public Player(int hp)
        {
            this.hp = hp;
            Console.WriteLine("Player hp 생성자 호출!");
        }
    }

    class Knight : Player // 자식 Class 또는 파생 Class
    {
        public Knight()
        {
            Console.WriteLine("Knight 생성자 호출!");
        }

        static public Knight CreateKnight()
        {
            Knight knight = new Knight();
            knight.hp = 100;
            knight.attack = 5;
            return knight;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = Knight.CreateKnight();
        }
    }
}​

 

상속시 생성자 응용 #3 및 base() 생성자 응용
namespace CSharpStudy
{
    class Player // 부모 Class 또는 기반 Class
    {
        static public int counter = 1;

        public int id;
        public int hp;
        public int attack;

        public Player()
        {
            Console.WriteLine("Player 생성자 호출!");
        }

        public Player(int hp)
        {
            this.hp = hp;
            Console.WriteLine("Player hp 생성자 호출!");
        }
    }

    class Knight : Player // 자식 Class 또는 파생 Class
    {
        public Knight() : base(100)
        {
            Console.WriteLine("Knight 생성자 호출!");
        }

        static public Knight CreateKnight()
        {
            Knight knight = new Knight();
            knight.hp = 100;
            knight.attack = 5;
            return knight;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = Knight.CreateKnight();
        }
    }
}​

 

순서대로 상속 응용 #1, #2, #3 의 결과

 

# 은닉성

- 은닉성은 OOP 특징 중 하나이다.

- 사용자에게 필요한 최소의 기능만을 노출하고 내부를 감추는 것이다.

접근 제한자를 통한 은닉성 응용
namespace CSharpStudy
{
    class Knight
    {
        int hp; // 접근 제한자를 선언하지 않은 경우 기본적으로 private

        public void SetHp(int hp)
        {
            this.hp = hp;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();
            knight.SetHp(100);
        }
    }
}​

 

+ 추가 검색 ( https://usingsystem.tistory.com/6 )

 - 은닉성 구현 방법

 1. 접근 제한자

 2. 프로퍼티

 3. 레코드

 4. 무명형식

 

# 클래스 형식 변환

- 자식 클래스 ~> 부모 클래스 변환은 문제가 발생하지 않는다.

- 부모 클래스 ~> 자식 클래스 변환은 문제가 발생한다.

 ~> 문제가 발생하는지는 프로그램을 실행해봐야만 확인이 가능하다.

 ~> 이를 is 연산자와 as 연산자를 통해 해결할 수 있다.

클래스 형식 변환 예시 (실행 결과는 오류 발생)
namespace CSharpStudy
{
    class Player
    {
        protected int hp;
        protected int attack;
    }

    class Knight : Player
    {

    }

    class Mage : Player
    {
        public int mp;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();
            Mage mage = new Mage();

            // 자식 클래스 -> 부모 클래스 변환은 문제 X
            Player player = knight;
            // 부모 클래스 -> 자식 클래스 변환은 문제 발생 
            Mage otherMage = (Mage)player; // 프로그램을 실행해봐야 오류 발견 가능
        }
    }
}​

 

is 연산자를 통한 클래스 형식 변환 예시 오류 수정
namespace CSharpStudy
{
    class Player
    {
        protected int hp;
        protected int attack;
    }

    class Knight : Player
    {

    }

    class Mage : Player
    {
        public int mp;
    }

    class Program
    {
        static void EnterGame(Player player)
        {
            // is 문법을 통한 오류 수정
            bool isMage = player is Mage;
            if (isMage)
            {
                Mage mage = (Mage)player;
                mage.mp = 10;
            }
        }

        static void Main(string[] args)
        {
            Knight knight = new Knight();
            Mage mage = new Mage();

            EnterGame(knight);
        }
    }
}​

 

as 연산자를 통한 클래스 형식 변환 예시 오류 수정
namespace CSharpStudy
{
    class Player
    {
        protected int hp;
        protected int attack;
    }

    class Knight : Player
    {

    }

    class Mage : Player
    {
        public int mp;
    }

    class Program
    {
        static void EnterGame(Player player)
        {
            // as 문법을 통한 오류 수정
            Mage mage = player as Mage;
            if (mage != null)
            {
                mage.mp = 10;
            }
        }

        static void Main(string[] args)
        {
            Knight knight = new Knight();
            Mage mage = new Mage();

            EnterGame(knight);
        }
    }
}​

 

+ 추가 검색 ( https://dybz.tistory.com/94 )

 - is 연산자

  ~> Casting 가능 여부만을 판단한다.

  ~> Casting이 가능한 경우 true를, 불가능한 경우 false를 return한다.

 - as 연산자

  ~> Casting시 사용한다.

  ~> Casting에 성공할 경우 Casting 결과를, 실패할 경우 null을 return한다.

 

# 다형성

- 다형성은 OOP 특징 중 하나이다.

- 다형성은 Object가 여러 형태를 가질 수 있다는 것을 의미한다.

- virtual Keyword는 자식 Class에서 재정의 (override) 를 할 수 있도록 만들어준다.

- override Keyword는 부모 Class에서 virtual이나 abstract로 선언된 메소드나 프로퍼티를 재정의 (override) 한다.

virtual Keyword와 override Keyword 응용
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{ 
    class Player
    {
        protected int hp;
        protected int attack;

        public virtual void Move()
        {
            Console.WriteLine("Player 이동!");
        }
    }

    class Knight : Player
    {
        public override void Move()
        {
            base.Move(); // 부모 Class가 가진 Move() 메소드를 실행
            Console.WriteLine("Knight 이동!");
        }
    }

    class Program
    {
        static void EnterGame(Player player)
        {
            player.Move(); // 매개변수의 Type은 Player지만 override를 통해 knight의 Move() 메소드가 실행
        }

        static void Main(string[] args)
        {
            Knight knight = new Knight();

            EnterGame(knight);
        }
    }
}​

 

+ 추가 검색 ( https://kimasill.tistory.com/entry/C-Abstract%EC%B6%94%EC%83%81Virtual%EA%B0%80%EC%83%81Interface%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4-%EC%B0%A8%EC%9D%B4 )

 - virtual과 abstract의 차이점

  ~> virtual Keyword 사용시 자식 Class에서 필요에 따라 재정의 (override) 할 수 있지만 필수는 아니다.

  ~> virtual Keyword 사용시 객체 생성이 가능하다.

  ~> abstract Keyword 사용시 abstract Class는 자체적으로 구현이 불가능하고, 자식 Class에서 반드시 구현해야 한다.

  ~> abstract Keyword 사용시 Class 또한 abstract으로 선언한다.

  ~> abstract Keyword 사용시 객체 생성이 불가능하다. (abstract class 를 상속받은 class는 객체 생성 가능)

 

+ 추가 검색 ( https://ssabi.tistory.com/49 )

 - overloading과 overriding의 차이

  ~> overloading의 사전적 의미는 "과적하다" 이다.

  ~> overloading은 하나의 메소드에 여러개의 구현을 과적할 수 있다.

  ~> overriding의 사전적 의미는 "더 중요한", "최우선시 되는" 이다.

  ~> overriding은 부모 Class에서 물려받은 메소드를 자식 Class에서 재정의하여 자식 클래스의 메소드가 더 우선시 된다.

 

# 문자열 둘러보기

string 관련 메소드들
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            string name = "Harry Potter";

            // 1. 찾기
            bool found = name.Contains("Harry");
            int index = name.IndexOf('P'); // 찾지 못할 경우 -1 을 return

            // 2. 변형
            name = name + " Junior";
            string lowerCaseName = name.ToLower();
            string upperCaseName = name.ToUpper();
            string newName = name.Replace('r', 'l');

            // 3. 분할
            string[] names = name.Split(new char[] { ' ' });
            string substringName = name.Substring(5);
        }
    }
}​

 


 

[ 섹션 5. TextRPG2 ]

# TextRPG2 플레이어 & 몬스터 생성

TextRPG2에서 모든 생명체를 위한 부모 Class Creature 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpStudy
{
    public enum CreatureType
    {
        None,
        Player = 1,
        Monster = 2
    }
    class Creature
    {
        CreatureType type;
        protected int hp = 0;
        protected int attack = 0;

        protected Creature(CreatureType type)
        {
            this.type = type;
        }

        public void SetInfo(int hp, int attack)
        {
            this.hp = hp;
            this.attack = attack;
        }

        public int GetHp() { return hp; }
        public int GetAttack() { return attack; }

        public bool IsDead() { return hp <= 0; }

        public void OnDamaged(int damage)
        {
            hp -= damage;
            if (hp < 0)
                hp = 0;
        }
    }
}​

 

Creature를 상속받는 Player 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpStudy
{
    public enum PlayerType
    {
        None = 0,
        Knight = 1,
        Archer = 2,
        Mage = 3
    }

    class Player : Creature
    {
        protected PlayerType type = PlayerType.None;

        // 매개변수를 받는 생성자를 생성시, 기본 생성자를 따로 만들어주지 않는 이상 기본 생성자는 사용 불가
        protected Player(PlayerType type) : base(CreatureType.Player)
        {
            this.type = type;
        }

        public PlayerType GetPlayerType() { return type; }
    }

    class Knight : Player
    {
        public Knight() : base(PlayerType.Knight)
        {
            SetInfo(100, 10);
        }
    }

    class Archer : Player
    {
        public Archer() : base(PlayerType.Archer)
        {
            SetInfo(75, 12);
        }
    }

    class Mage : Player
    {
        public Mage() : base(PlayerType.Mage)
        {
            SetInfo(50, 15);
        }
    }
}​

 

Creature를 상속 받는 Monster 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpStudy
{
    public enum MonsterType
    {
        None = 0,
        Slime = 1,
        Orc = 2,
        Skeleton = 3
    }
    class Monster : Creature
    {
        protected MonsterType type;
        protected Monster(MonsterType type) : base(CreatureType.Monster)
        {
            this.type = type;
        }

        public MonsterType GetMonsterType() { return type; }
    }

    class Slime : Monster
    {
        public Slime() : base(MonsterType.Slime) 
        {
            SetInfo(10, 1);
        }
    }

    class Orc : Monster
    {
        public Orc() : base(MonsterType.Orc)
        {
            SetInfo(20, 2);
        }
    }

    class Skeleton : Monster
    {
        public Skeleton() : base(MonsterType.Skeleton)
        {
            SetInfo(15, 5);
        }
    }
}​

 

TextRPG2 플레이어 & 몬스터 생성
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            Player player = new Knight();
            Monster monster = new Orc();

            int damage = player.GetAttack();
            monster.OnDamaged(damage);
        }
    }
}​

 

# TextRPG2 게임 진행 & 마무리

TextRPG2 게임 진행을 위한 Game 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpStudy
{
    public enum GameMode
    {
        None,
        Lobby,
        Town,
        Field
    }

    // 게임 진행과 관련된 전반적인 사항들을 관리
    class Game
    {
        private GameMode mode = GameMode.Lobby;
        private Player player = null;
        private Monster monster = null;
        Random rand = new Random();

        public void Process()
        {
            switch (mode)
            {
                case GameMode.Lobby:
                    ProcessLobby();
                    break;
                case GameMode.Town:
                    ProcessTown();
                    break;
                case GameMode.Field:
                    ProcessField();
                    break;
            }
        }

        private void ProcessLobby()
        {
            Console.WriteLine("직업을 선택하세요");
            Console.WriteLine("[1] 기사");
            Console.WriteLine("[2] 궁수");
            Console.WriteLine("[3] 법사");

            string input = Console.ReadLine();
            switch (input)
            {
                case "1":
                    player = new Knight();
                    mode = GameMode.Town;
                    break;
                case "2":
                    player = new Archer();
                    mode = GameMode.Town;
                    break;
                case "3":
                    player = new Mage();
                    mode = GameMode.Town;
                    break;
            }
        }

        private void ProcessTown()
        {
            Console.WriteLine("마을에 입장했습니다!");
            Console.WriteLine("[1] 필드로 가기");
            Console.WriteLine("[2] 로비로 돌아가기");

            string input = Console.ReadLine();
            switch (input)
            {
                case "1":
                    mode = GameMode.Field;
                    break;
                case "2":
                    mode = GameMode.Lobby;
                    break;
            }
        }

        private void ProcessField()
        {
            Console.WriteLine("필드에 입장했습니다!");
            Console.WriteLine("[1] 싸우기");
            Console.WriteLine("[2] 일정 확률로 마을 돌아가기");

            CreateRandomMonster();

            string input = Console.ReadLine();
            switch (input)
            {
                case "1":
                    ProcessFight();
                    break;
                case "2":
                    TryEscape();
                    break;
            }
        }

        private void CreateRandomMonster()
        {
            int randValue = rand.Next(0, 3);
            switch (randValue)
            {
                case 0:
                    monster = new Slime();
                    Console.WriteLine("슬라임이 생성되었습니다!");
                    break;
                case 1:
                    monster = new Orc();
                    Console.WriteLine("오크가 생성되었습니다!");
                    break;
                case 2:
                    monster = new Skeleton();
                    Console.WriteLine("스켈레톤이 생성되었습니다!");
                    break;
            }
        }

        private void ProcessFight()
        {
            while (true)
            {
                int damage = player.GetAttack();
                monster.OnDamaged(damage);
                if (monster.IsDead())
                {
                    Console.WriteLine("승리했습니다");
                    Console.WriteLine($"남은 체력 : {player.GetHp()}");
                    break;
                }

                damage = monster.GetAttack();
                player.OnDamaged(damage);
                if (player.IsDead())
                {
                    Console.WriteLine("패배했습니다");
                    mode = GameMode.Lobby;
                    break;
                }
            }
        }

        private void TryEscape()
        {
            int randValue = rand.Next(0, 101);
            if (randValue < 33)
            {
                mode = GameMode.Town;
            }
            else
            {
                ProcessFight();
            }
        }
    }
}

 

TextRPG2 게임 진행
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{ 
    class Program
    {
        static void Main(string[] args)
        {
            Game game = new Game();

            while (true)
            {
                game.Process();
            }
        }
    }
}

 


 

[ 섹션 6. 자료구조 맛보기 ]

# 배열

- 배열은 참조 타입이다.

- foreach문은 배열, Collection 안의 일련의 데이터들을 차례로 처리하기 위해 사용되는 반복문이다.

배열 및 foreach문 응용
namespace CSharpStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            // 배열 (참조 타입)
            int[] MathScores = new int[] { 10, 20, 30, 40, 50 };
            int[] EngScores = { 40, 50, 70, 90 };

            for (int i = 0; i < MathScores.Length; i++)
            {
                Console.WriteLine("MathScore : " + MathScores[i]);
            }

            foreach (int score in EngScores)
            {
                Console.WriteLine($"EngScore : {score}");
            }
        }
    }
}​

 

# 다차원 배열

다차원 배열 응용 #1
namespace CSharpStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            int[ , ] Array_1 = new int[ , ] { { 1, 2, 3 }, { 4, 5, 6 } };
            int[ , ] Array_2 = { { 1, 2, 3 }, { 4, 5, 6 } };
        }
    }
}​

 

다차원 배열 응용 #2
namespace CSharpStudy
{
    class Map
    {
        int[,] tiles =
        {
            { 1, 1, 1, 1, 1 },
            { 1, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 1 },
            { 1, 0, 0, 0, 1 },
            { 1, 1, 1, 1, 1 }
        };

        public void Render()
        {
            ConsoleColor defaultColor = Console.ForegroundColor;
            for (int y = 0; y < tiles.GetLength(0); y++)
            {
                for (int x = 0; x < tiles.GetLength(1); x++)
                {
                    if (tiles[y, x] == 1)
                        Console.ForegroundColor = ConsoleColor.Red;
                    else
                        Console.ForegroundColor = ConsoleColor.Green;
                    Console.Write("\u25cf");
                }
                Console.WriteLine();
            }
            Console.ForegroundColor = defaultColor;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Map map = new Map();
            map.Render();
        }
    }
}

 

가변 배열 응용
namespace CSharpStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            int[][] a = new int[3][];
            a[0] = new int[3];
            a[1] = new int[6];
            a[2] = new int[2];

            a[0][0] = 1;
        }
    }
}​

 

# List

- List는 참조 타입이다.

- List는 배열과 달리 동적으로 크기 조절이 가능하다. 따라서 배열의 크기에 대해 크게 신경 쓸 필요가 없다.

 ~> 배열과 마찬가지로 인덱스를 사용하여 요소에 접근할 수 있다. (List가 최소 1개 이상의 데이터를 가져야 접근 가능)

List 응용
namespace CSharpStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int>();
            for (int i = 0; i < 5; i++)
                list.Add(i); // list의 맨 뒤에 데이터 추가

            list.Insert(2, 999); // index를 통한 데이터 삽입

            list.Remove(3); // 값을 통한 데이터 삭제 (여러개 존재할 경우 가장 처음으로 발견한 데이터를 삭제)

            list.RemoveAt(0); // index를 통한 데이터 삭제

            list.Clear(); // list를 초기화
            
            for (int i = 0; i < list.Count; i++) // list의 크기는 Length를 사용하는 배열과 달리 Count 사용
                Console.WriteLine(list[i]);

            foreach (int num in list)
                Console.WriteLine(num);
        }
    }
}​

 

# Dictionary

- Dictionary는 참조 타입이다.

- Dictionary는 인덱스 대신 Key 값을 통해 Value 값을 찾는다.

- Hash Table을 사용하기 때문에 빠르지만, 메모리 낭비가 심하다.

Dictionary 응용
namespace CSharpStudy
{
    class Monster
    {
        public int id;
        public Monster(int id)
        {
            this.id = id;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<int, Monster> dic = new Dictionary<int, Monster>();
            for (int i = 0; i < 10000; i++)
            {
                dic.Add(i, new Monster(i));
            }

            Monster monster;
            bool isFound = dic.TryGetValue(5000, out monster);

            dic.Remove(7777);
            dic.Clear();
        }
    }
}​

 

+ 추가 검색 ( https://coding-shop.tistory.com/53 )

 - ContainsKey() 와 TryGetValue() 차이점

  ~> ContainsKey() 와 TryGetValue() 는  Dictionary에 Key 존재 여부를 판단하고 bool Type을 return 한다.

  ~> ContainsKey() 는 Key와 연결된 Value 값을 출력하지 않는다.

  ~> TryGetValue() 는 Key와 연결된 Value 값을 출력한다.

 


 

[ 섹션 7. 알아두면 유용한 기타 문법 ]

# Generic (일반화)

- Generic은 특정 데이터 타입에 국한되지 않고 모든 타입을 허용하는 Generic Class와 메소드를 구현할 수 있다.

 ~> 이때 특정 조건에만 대응되는 데이터 타입이 필요한 경우가 있는데, 이는 where Keyword를 사용하여 제약 조건을 

      추가할 수 있다.

제약 조건 종류 - (출처) https://developer-talk.tistory.com/209

Generic Class 응용
namespace CSharpStudy
{
    class Program
    {
        class MyList<T>
        {
            T[] arr = new T[10];

            public T GetItem(int i)
            {
                return arr[i];
            }
        }

        static void Main(string[] args)
        {
            MyList<int> myIntList = new MyList<int>();
            int intItem = myIntList.GetItem(0);

            MyList<float> myFloatList = new MyList<float>();
            float floatItem = myFloatList.GetItem(0);
        }
    }
}​

 

Generic Function 응용
namespace CSharpStudy
{
    class Program
    {
        static void GenericFunc<T>(T input)
        {
            Console.WriteLine(input);
        }

        static void Main(string[] args)
        {
            GenericFunc<int>(3);
            GenericFunc<float>(5.0f);
        }
    }
}​

 

+ 추가 검색 ( https://velog.io/@livelyjuseok/C-Object-%ED%83%80%EC%9E%85%EA%B3%BC-%EB%B0%95%EC%8B%B1-%EC%96%B8%EB%B0%95%EC%8B%B1 )

 - Object 타입은 모든 타입 (int, float ...) 부터 참조 (Class, string ...) 그리고 우리가 만들어내는 모든 데이터 Type들까지도

  객체를 담을 수 있다.

  ~> 이러한 이유는 C#에서는 모든 데이터 Type이 Object를 상속 받도록 구조가 짜여져 있기 때문이다.

  ~> 하지만 마구잡이로 사용시 메모리 낭비가 발생하기 때문에 Boxing과 UnBoxing의 개념을 이해해야 한다.

 -  Boxing이란 Stack 영역에 저장된 값 타입의 데이터를 Object 타입을 통해 Heap 영역에 저장하는 과정을 말한다.

  ~> 말 그대로 박스로 감싸는 과정이며, Object에 값 타입의 int 값을 넣으면 Stack 영역에 저장되어야 하는 값이 Box로

       감싸져 Heap 영역에 저장된다.

 - UnBoxing은 이렇게 Box로 감싸진 데이터를 풀어내는 과정을 말한다.

 - Object obj = 5 가 Boxing의 예시, int a = (int)obj 가 UnBoxing의 예시이다.

 

# Interface (인터페이스)

- interface의 특징

  ~> C#은 다중 상속을 지원하지 않지만, interface는 다중 상속이 가능하다.

  ~> interface는 멤버 변수를 포함할 수 없다.

  ~> interface는 접근 제한 한정자를 사용할 수 없고, 구현부가 존재하지 않는다. (즉, 자체적으로 구현이 불가능)

  ~> interface를 상속받는 Class는 반드시 interface의 모든 메소드를 override (재정의) 해야 한다.

       (override시 모든 메소드는 public 으로 선언)

  ~> interface는 객체 생성이 불가능하지만, interface를 상속 받은 Class는 객체 생성이 가능하다.

 

+ 추가 정리

- virtual과 abstract과 interface의 차이

virtual의 특징
namespace CSharpStudy
{
    class Monster
    {
        int hp;
    
        public virtual void Shout()
        {
            Console.WriteLine("Monster의 Shout!");
        }
    }

    class Orc : Monster
    {
        public override void Shout()
        {
            Console.WriteLine("Orc의 Shout!");
        }
    }

    class Skeleton : Monster
    {
        // virtual에 대한 override는 필수 X
    }

    class Program
    {
        static void Main(string[] args)
        {
            // virtual은 객체 생성 가능
            Monster monster = new Monster();
        }
    }
}​

 

abstract의 특징
namespace CSharpStudy
{
    abstract class Monster
    {
        int hp;
    
        public abstract void Shout(); // 자체적으로는 구현 불가능
    }

    class Orc : Monster
    {
        public override void Shout() // abstract에 대한 override는 필수
        {
            Console.WriteLine("Orc의 Shout!");
        }
    }

    class Skeleton : Monster
    {
        public override void Shout() // abstract에 대한 override는 필수
        {
            Console.WriteLine("Skeleton의 Shout!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // abstract은 객체 생성 불가능
            // Monster monster = new Monster();
        }
    }
}​

 

interface의 특징
namespace CSharpStudy
{
    abstract class Monster
    {
        int hp;

        public abstract void Shout();
    }

    interface IFlyable
    {
        // 멤버 변수 포함 불가능
        // bool isFly;

        void Fly(); // 접근 제한 한정자 사용 불가능, 자체적으로는 구현 불가능
    }

    interface IRunable
    {
        // 멤버 변수 포함 불가능
        // int speed;

        void Run(); // 접근 제한 한정자 사용 불가능, 자체적으로는 구현 불가능
    }

    class Orc : Monster
    {
        public override void Shout()
        {
            Console.WriteLine("Orc의 Shout!");
        }
    }

    class Skeleton : Monster
    {
        public override void Shout()
        {
            Console.WriteLine("Skeleton의 Shout!");
        }
    }

    class FlyableOrc : Orc, IFlyable, IRunable // interface는 다중 상속 가능
    {
        public void Fly() // 접근 제한 한정자는 반드시 public
        {
            Console.WriteLine("Orc의 Fly!"); // interface에 대한 override는 필수
        }

        public void Run() // 접근 제한 한정자는 반드시 public
        {
            Console.WriteLine("Orc의 Run!"); // interface에 대한 override는 필수
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // interface는 객체 생성 불가능
            // IFlyable iflyable = new IFlyable();

            // interface를 상속 받은 Class의 객체 생성은 가능
            FlyableOrc flyableOrc = new FlyableOrc();
        }
    }
}

 

+ 추가 검색 ( https://yasic-or-nunch.tistory.com/23 )

 

 - C#은 다중 상속을 지원하지 않는다. 이러한 이유는 "죽음의 다이아몬드" 때문이다.

 - 죽음의 다이아몬드란?

  ~> 1개의 부모 Class를 2개의 자식 Class가 상속 받고, 2개의 자식 Class를 다시 1개의 자식 Class가 상속 받는 것이다.

  ~> 위의 그림에서 ComboDrive가 burn() 함수를 호출시 어떤 부모 Class의 burn() 함수를 실행 시킬지 모호하다.

 

# Property (프로퍼티)

- Property는 OOP의 특징 중 하나인 은닉성을 위해 사용한다.

 ~> Property는 get과 set을 통해 private로 선언된 변수에 접근이 가능하도록 한다.

 ~> 정보 은닉을 위해 private로 선언하였으나, get과 set을 통해 변수에 접근할 수 있더라도 Property를 사용하는 이유는

      변수의 값을 변경하거나 가져올 때 조건을 걸어서 변수의 접근을 제어할 수 있기 때문이다.

Property 응용
namespace CSharpStudy
{
    class Knight
    {
        private int hp;
        public int Hp
        {
            get { return hp; }
            set { hp = value; } // 기본적으로 매개변수는 value Keyword가 제공된다.
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();

            knight.Hp = 100;
            int hp = knight.Hp;
        }
    }
}​

 

 자동 구현 Property 응용
namespace CSharpStudy
{
    class Knight
    {
        public int HP { get; set; } = 100
    }

    class Program
    {
        static void Main(string[] args)
        {
            Knight knight = new Knight();

            int hp = knight.HP;
        }
    }
}​

 

# Delegate (대리자)

- Delegate는 메소드 자체를 인자로 넘겨주는 형식이다. (함수가 아닌 형식 (int, string ...) 인 것에 유의)

 ~> 버튼 클릭시 발생하는 일들을 순차적으로 코딩할 경우 UI에 관련된 Logic과 게임과 관련된 Logic 사이에

      간섭 및 복잡성이 발생하는 문제가 있다. 따라서 최대한 분리를 시켜 관리해야 장기적인 차원에서 유리하다.

 ~> Unity 자체에서 제공하는 Event 메소드들은 자체적으로 수정할 수 없다.

 ~> 위와 같은 문제들을 메소드 자체를 인자로 넘겨주는 Delegate 를 통해 해결할 수 있다.

- Delegate 목록에 실행할 메소드들을 등록 해두면 해당 Delegate가 등록된 함수들을 연쇄적으로 대신 실행시켜준다.

- Delegate 선언시 등록할 수 있는 함수의 조건을 제한한다. (매개변수, 반환값)

- Delegate를 직접 선언하지 않아도, C#에서 제공하는 Func, Action을 통해 Delegate 사용이 가능하다.

 ~> Anonymous Function (무명함수 or 익명함수) 을 통해 작성한다.

 ~> 반환값이 존재할 경우 Func을, 반환값이 존재하지 않을 경우 Action을 사용한다.

      (Func의 < > 안에 들어있는 n개의 형식 중 마지막은 반환값, 나머지는 인자에 해당)

Delegate 응용
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{ 
    class Program
    {
        // delegate : 메소드 자체를 인자로 넘겨주는 형식
        // 인자 : X, 반환값 : int
        // OnClicked : delegate 형식의 이름
        delegate int OnClicked();

        static void ButtonPressed (OnClicked clickedFunction)
        {
            clickedFunction();
        }

        static int TestDelegate()
        {
            Console.WriteLine("Hello Delegate");
            return 0;
        }

        static int TestDelegate2()
        {
            Console.WriteLine("Hello Delegate2");
            return 0;
        }


        static void Main(string[] args)
        {
            // delegate 사용 방법 #1
            ButtonPressed(TestDelegate);
            ButtonPressed(TestDelegate2);

            // delegate 사용 방법 #2 (객체 생성 방법)
            OnClicked clicked = new OnClicked(TestDelegate);
            clicked += TestDelegate2; // 객체 생성시 delegate chaining 가능
            clicked();

            // delegate 사용 방법 #3
            OnClicked clicked2 = new OnClicked(TestDelegate);
            clicked2 += TestDelegate2; // 객체 생성시 delegate chaining 가능
            ButtonPressed(clicked2);
        }
    }
}​

 

# Event (이벤트)

- Event는 Observer Pattern 을 사용한다.

 ~> Observer Pattern 이란 한 객체의 상태가 바뀌면 해당 객체에 의존하는 다른 객체들한테 연락이 가서 자동으로 내용이

      갱신되는 방식으로 1:N 의존성을 정의한다.

- delegate는 외부에서 호출이 가능하지만, event는 호출이 불가능하다. (구독 신청만 가능)

Event 응용을 위한 InputManager 생성
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace CSharpStudy
{
    internal class InputManager
    {
        // delegate와 event의 접근 제한 한정자는 동일하게 설정
        public delegate void OnInputKey();
        public event OnInputKey InputKey;

        public void Update()
        {
            if (Console.KeyAvailable == false)
                return;

            ConsoleKeyInfo info = Console.ReadKey();
            if (info.Key == ConsoleKey.A)
            {
                // 해당 event를 구독 신청한 구독자들에게 알려준다.
                InputKey();
            }
        }
    }
}​

 

Event 응용
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{
    class Program
    {
        static void Main(string[] args)
        {
            static void OnInputTest()
            {
                Console.WriteLine("Input Received!");
            }

            InputManager inputManager = new InputManager();
            inputManager.InputKey += OnInputTest; // OnInputTest 메소드가 InputKey event를 구독 신청

            while (true)
            {
                inputManager.Update();

                // event는 외부에서 호출이 불가능 (delegate와 가장 큰 차이점)
                // inputManager.InputKey();
            }
        }
    }
}​

 

# Lambda (람다식)

- Lambda 를 사용하는 이유는 대체로 간결하고 가독성이 높은 코드 작성을 위해 사용된다.

 ~> 또한 복잡한 코드를 더욱 쉽게 작성하여 개발자의 생산정을 높여주고, 코드의 유지 보수성을 높일 수 있다.

무명 함수 및 Lambda 응용
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{
    enum ItemType
    {
        Weapon,
        Armor,
        Amulet,
        Ring
    }

    enum Rarity
    {
        Normal,
        Uncommon,
        Rare
    }

    class Item
    {
        public ItemType itemType;
        public Rarity rarity;
    }

    class Program
    {
        static List<Item> _items = new List<Item>();

        delegate bool ItemSelector(Item item);

        static Item FindItem(ItemSelector selector)
        {
            foreach (Item item in _items)
            {
                if (selector(item))
                    return item;
            }
            return null;
        }

        static void Main(string[] args)
        {
            _items.Add(new Item() { itemType = ItemType.Weapon, rarity = Rarity.Normal });
            _items.Add(new Item() { itemType = ItemType.Armor, rarity = Rarity.Uncommon });
            _items.Add(new Item() { itemType = ItemType.Ring, rarity = Rarity.Rare });

            // Anonymous Function (무명 함수 or 익명 함수)
            Item item = FindItem(delegate (Item item) { return item.itemType == ItemType.Weapon; });

            // Lambda (람다식)
            Item item2 = FindItem((Item item) => { return item.itemType == ItemType.Weapon; });
        }
    }
}​

 

Func 및 Lambda 응용
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{
    enum ItemType
    {
        Weapon,
        Armor,
        Amulet,
        Ring
    }

    enum Rarity
    {
        Normal,
        Uncommon,
        Rare
    }

    class Item
    {
        public ItemType itemType;
        public Rarity rarity;
    }

    class Program
    {
        static List<Item> _items = new List<Item>();

        static Item FindItem(Func<Item, bool> selector)
        {
            foreach (Item item in _items)
            {
                if (selector(item))
                    return item;
            }
            return null;
        }

        static void Main(string[] args)
        {
            _items.Add(new Item() { itemType = ItemType.Weapon, rarity = Rarity.Normal });
            _items.Add(new Item() { itemType = ItemType.Armor, rarity = Rarity.Uncommon });
            _items.Add(new Item() { itemType = ItemType.Ring, rarity = Rarity.Rare });

            // Anonymous Function (무명 함수 or 익명 함수)
            Item item = FindItem(delegate (Item item) { return item.itemType == ItemType.Weapon; });

            // Lambda (람다식)
            Item item2 = FindItem((Item item) => { return item.itemType == ItemType.Weapon; });
        }
    }
}​

 

# Exception (예외 처리)

- Exception은 throw-try-catch-finally 문을 통해 사용할 수 있다.

 ~> throw 문을 통해 예외를 던진다.

 ~> try 문에 예외가 발생할 수 있는 소스코드를 작성한다.

 ~> catch 문에 예외를 처리하는 소스코드를 작성한다.

 ~> finally 문에 예외 발생 여부와 상관없이 항상 실행되는 소스코드를 작성한다.

- 발생한 예외가 특정 catch 문에 해당할 경우 해당 catch 문을 제외한 나머지 catch 문은 실행되지 않는다.

- 모든 예외는 Exception Class를 상속 받는다.

 ~> 따라서 catch 문 간의 순서가 중요하다.

     (Exception catch문이 catch문 중 가장 위에 위치할 경우 아래의 catch문들은 평생 실행되지 X)

- 게임 업계에서 게임 Logic에 대해서는 Exception 을 잘 사용하지 X

 ~> Exception 을 사용하여 예외를 처리하기보다는 Crash를 통한 오류 수정이 더욱 중요하다.

Exception 응용
using System.ComponentModel;
using System.Diagnostics.Tracing;
using System.Numerics;

namespace CSharpStudy
{
    class Program
    {
        class TestException : Exception
        {
            int a, b, result;

            public TestException()
            {
                this.a = 10;
                this.b = 0;
                this.result = a / b;
            }
        }

        static void Main(string[] args)
        {
            try
            {
                throw new TestException();
            }
            catch (DivideByZeroException e)
            {
                Console.WriteLine("0으로 나눌 수 없습니다!");
            }
            catch (Exception e) 
            {
                Console.WriteLine("또 다른 예외가 발생했습니다!");
            }
            finally
            {
                Console.WriteLine("반드시 실행되는 구문!");
            }
        }
    }
}​

 

# Reflection (리플렉션)

- C#은 Reflection 기능을 지원한다. 이는 객체(Instance)를 토대로 데이터타입의 메타적인 정보를 가져오는 기법이다.

 ~> Reflection은 조사, Instance 생성, 기존 개체에서 형식을 가져와 호출, 접근 기능을 제공한다.

 ~> using System.Reflection; 을 통해 Reflection 기능을 사용할 수 있다.

- Attribute는 코드에 대한 부가 정보를 기록하고 읽을 수 있는 기능이다.

 ~> 주석은 사람이 작성하고 사람이 읽는 정보라면, Attribute는 사람이 작성하고 컴퓨터가 읽는 정보이다.

 ~> Attribute는 Metadata 형식으로 저장되며, Compile time, Runtime 시에도 컴퓨터에게 정보를 제공한다.

 ~> 미리 구현된 Attribute도 존재하며, 직접 Attribute를 만들 수도 있다.

      (Unity에서 자주 사용하는 Attribute는 대표적으로 [ SerializeField ] 가 존재)

Reflection 및 Attribute 응용
using System.ComponentModel;
using System.Diagnostics.Tracing;
using System.Numerics;
using System.Reflection;

namespace CSharpStudy
{
    class Program
    {
        class Important : System.Attribute // Custom Attribute는 System.Attribute를 상속 받아 구현
        {
            string msg;

            public Important(string msg) { this.msg = msg; }
        }


        class Monster
        {
            [Important("My Important variable")]
            public int hp;

            protected int attack;
            private float speed;

            void Attack() { }
        }

        static void Main(string[] args)
        {
            // Reflection : X-Ray
            Monster monster = new Monster();
            Type type = monster.GetType();

            FieldInfo[] fields = type.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic);

            foreach (FieldInfo field in fields)
            {
                string access = "None";
                if (field.IsPublic)
                    access = "Public";
                else if (field.IsPrivate)
                    access = "Private";

                var attributes = field.GetCustomAttributes();

                Console.WriteLine($"{access} {field.FieldType.Name} {field.Name}");
            }
        }
    } 
}​

 

#  Nullable (널러블)

- Nullable은 Null을 가질 수 없는 데이터 타입을 Null을 가질 수 있는 타입으로 만든 새로운 타입이다.

 ~> 일반적으로 값 타입들 (int, double, bool, struct ...) 이 Null을 가질 수 없다.

Nullable 응용
using System.ComponentModel;
using System.Diagnostics.Tracing;
using System.Numerics;
using System.Reflection;

namespace CSharpStudy
{
    class Monster
    {
        public int Id { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // Nullable -> Null + able
            int? number = null;
            int value;

            Monster monster = null;

            // int number의 Null 확인 방법 #1
            if (number != null)
            {
                value = number.Value;
            }

            // int number의 Null 확인 방법 #2
            if (number.HasValue)
            {
                value = number.Value;
            }

            // int number의 Null 확인 방법 #3
            value = number ?? 0; // number가 Null이 아닐 경우 해당 값으로, Null일 경우 0으로 초기화

            // 참조 타입인 class에서도 사용이 가능하다.
            // Monster monster의 Null 확인 방법 #1 (아래의 주석 처리된 코드들을 한 줄로 끝낼 수 있다.)
            int? id = monster?.Id; // monster가 Null이 아닐 경우 해당 값으로, Null일 경우 null로 초기화

            // if (monster != null)
            //     int id = monster.Id;
            // else
            //     int id = 0;
        }
    } 
}​

 

 

 

 

[ 추가 지식 ]

# 코루틴과 Invoke는 MonoBehaviour을 상속 받는 Class에서만 사용 가능하다.

# 코루틴에 매개변수 전달시 StartCoroutine("FunctionName", 매개변수) 를 통해 전달할 경우 1개의 매개변수만 전달 가능

 ~> 2개 이상의 매개변수 전달시 StartCoroutine(FunctionName(매개변수, 매개변수)) 와 같이 코루틴을 호출
# GameObject가 비활성화인 상태에서는 Find 함수를 통해 찾을 수 없다.
 ~> 활성화된 부모를 찾아 자식을 찾는 형식으로 접근해야 한다.
# transform.position을 통해 Player를 움직일 경우 떨림 현상이 발생한다.
 ~> 물리 계산 과정에서 발생하는 현상으로 rigid.MovePosition 를 통해 해결할 수 있다. (rigid는 Rigidbody Component)
# 같은 Canvas 안에서의 UI는 Hierarchy 창에서 밑에 있을수록 출력 우선순위가 높다.
 ~> 다른 Canvas 간의 UI 출력 우선순위는 Hierarchy 창을 통해 변경할 수 없고 Sort Order를 통해 변경할 수 있다.
# Awake()는 일반적으로 게임이 시작되기 전에, 모든 변수와 게임의 상태를 초기화하기 위해서 호출된다.
  Start()는 Behaviour의 주기 동안에 1번만 호출된다. (즉, Script Instance로 활성화된 경우에만 실행된다.)

# UI는 Component로 Transform이 아닌 RectTransform을 가진다.

 ~> 이를 단순히 transform.position 를 통해 이동시 전혀 의도치 않은 방식으로 이동하게 된다.

 ~> GetComponent<RectTransform>().anchoredPosition 을 통해 이동시 올바르게 이동한다.

      (이는 Anchor 를 기준으로 position을 결정하는 것)

# string 비교시 == 는 참조를, Equals()는 값을 비교한다.

 

 

 

[ 인벤토리 생성 ]

 

우선 인벤토리를 위한 Panel을 생성한 뒤

생성한 Panel 아래에 [ 오른쪽 마우스 ] - [ UI ] - [ Scroll View ] 를 생성하고

생성한 Scroll View를 인벤토리 Panel에 맞춰 크기를 조절한다.

 

 

Scroll View의 Scroll Bar는 필요없기 때문에 삭제한다.

 

 

Scroll View 아래의 Viewport 아래의 Content Object에 Button을 여러개 추가한 뒤 (정렬 확인 용도)

Content Object에 Grid Layout Group Component를 추가하여

Cell Size와 Spacing의 조절을 통해 자식 Object를 정렬한다.

또한 Constraint는 Flexible이 아닌 Fixed Column Count로 변경하여 열의 개수를 제한한다.

 

 

정렬이 끝났으니 버튼을 1개만 남기고 나머지는 전부 지운 다음

Outline Component를 추가하거나 Sprite를 변경하는 등 버튼을 꾸민다.

 

그 후 버튼 아래의 Text는 필요가 없으므로 지우고

버튼의 이름을 Slot으로 변경한 뒤

아래에 [ 오른쪽 마우스 ] - [ UI ] - [ Image ] 를 통해 Image를 추가한다.

( 해당 image는 Slot에 아이템이 들어올 경우 띄워주는 역할 )

 

 

Image의 이름을 ItemImage로 변경한 뒤

해당 Slot을 프리팹화한다.

 

 

그 후 Slot을 Ctrl + D를 통해

여러개 복제하면 인벤토리 완성이다.

 

 

만약 Scroll View가 필요없는 Inventory일 경우

인벤토리를 위한 Panel에 Grid Layout Group Component를 추가하고

Panel 바로 밑에 Slot들을 생성하면 된다.

 

 

 

[ 2D Tilemap Sprite를 Slice 하는 방법 ]

1. 우선 Slice 하고자 하는 Sprite의 Sprite Mode를 Multiple로 변경한 뒤 Sprite Editor를 클릭

2. 왼쪽 위 [ Slice ] 를 클릭한 뒤 Type을 Grid By Cell Size로 변경한 뒤 수치를 적절히 조정 후 Slice 클릭

3. Sprite의 Advanced에서 Filter Mode를 Point (no filter)로 바꾸고 Compression을 None으로 바꿔야 화질이 선명하다.

 

[ Tile Palette 사용 방법 ]

1. [ Window ] - [ 2D ] - [ Tile Palette ] 를 통해 사용 가능하다.

2. Create New Palette 를 통해 새로운 Tile Palette 생성이 가능하다.

3. 사용하고자 하는 Tile, Sprite, Sprite Texture를 끌어 Tile Palette에 옮긴다.

( 참고 : 첫번째는 타일 선택 / 두번째는 타일 이동 / 세번째는 선택한 타일로 페인트 칠하기 )

( 참고 : Palette 수정시 Edit 선택, 페인트 칠시 Edit 선택 해제 )

4. [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 2D Object ] - [ Tilemap ] 을 통해 Tilemap 생성이 가능하다.

5. 생성한 Tilemap 위에 원하는 Tile을 페인트 칠한다.

 

< 만약 Player가 해당 타일 위를 밟고 이동할 경우 아래 과정을 수행 >

6. 4번에서 생성한 Tilemap에 페인트 칠을 한 뒤 Tilemap Collider 2D Component를 추가한다.

    (Tilemap Collider 2D Component는 Tilemap에 맞춰 Collider를 생성)

7. 6번에서 추가한 Tilemap Collider 2D Component 추가시 생성된 Collider는 각 타일마다 따로 생성된다. 이렇게 분할된

    Collider는 퍼포먼스 상의 문제, 끼임 문제 등이 발생할 수 있다.

8. 따라서 이를 해결하기 위해 제공되는 Composite Collider 2D Component를 같이 추가해주는 것이 좋다.

9. Composite Collider 2D Component 추가시 Rigidbody 2D Component가 자동으로 함께 추가되는데 Rigidbody 2D의

    Type을 Dynamic이 아닌 Static으로 바꿔준다. (Dynamic일 경우 중력 작용으로 아래로 무한히 추락함)

 

[ 2D Tilemap Extras 추가 방법 ]

원래 Unity에서 2D Tilemap Extras를 공식 지원하지 않았는데 이제 공식 지원을 해준다.

 > 이는 2D Tilemap 기능과 함께 사용하기 위한 추가 스크립트가 포함된 패키지로 Tilemap 기능을 위한 Custom Tile과

    Brush를 제공한다.

 

[ Edit ] - [ Project Settings ] - [ Package Manager ] - [ Advanced Settings ] 의 Enabel Pre-release Packages를 체크한 뒤

[ Window ] - [ Package Manager ] 에서 packages를 Unity Registry로 바꾼 뒤 2D Tilemap Extras를 import 할 수 있다.

 

[ 2D Tilemap Extras 사용 방법 ]

Project 창에서 [ 오른쪽 마우스 ] - [ Create ] - [ 2D ] - [ Tiles ] 에서 생성 가능하다.

 

[ Rule Tile 사용 방법 ]

# 일반적인 Rule Tile 사용 방법

1. Project 창에서 [ 오른쪽 마우스 ] - [ Create ] - [ 2D ] - [ Tiles ] - [ Rule Tile ] 에서 사용 가능하다.

2. +를 통해 Number of Tiling Rules를 추가할 수 있다. ( 즉, 규칙의 개수를 추가 가능 )

3. 인접한 Tile이 존재하는 곳은 왼쪽 마우스를 통해 초록색 화살표를,

    존재하지 않는 곳은 오른쪽 마우스를 통해 빨간색 X를 입력하여 Rule을 설정할 수 있다.

4. 설정을 끝마친 Rule Tile을 Tile Palette로 끌어다가 추가한 뒤 사용할 수 있다.

 

5. [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 2D Object ] - [ Tilemap ] 을 통해 Tilemap을 생성하여 페인트 칠을 하면 아래와 같이

   설정한 Rule에 따라 잘 동작하는 것을 볼 수 있다.

 

6. 위의 과정을 반복하여 여러개의 Tilemap을 생성시 Tilemap의 [ Inspector ] - [ Tilemap Renderer ] - [ Additional Settings ]

    의 Order in Layer를 적절하게 설정한다. (ex : 땅 Tilemap의 Order in Layer는 0, 꽃 Tilemap의 Order in Layer는 1)

     

# Rule Tile을 사용한 Animation Tile 생성 방법

1. Project 창에서 [ 오른쪽 마우스 ] - [ Create ] - [ 2D ] - [ Tiles ] - [ Rule Tile ] 에서 사용 가능하다.

2. +를 통해 Number of Tiling Rules를 추가할 수 있다.

3. Rule의 Output을 Single이 아닌 Animation으로 설정한다.

4. Animation으로 사용할 Sprite의 개수만큼 Size를 설정한 뒤 Sprite를 드래그 앤 드롭으로 연결해준다.

5. Min Speed와 Max Speed를 통해 Animation의 속도를 조절할 수 있다.

     

# 외부 경계 설정 방법

1. Border 역할을 할 Tilemap을  [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 2D Object ] - [ Tilemap ] 을 통해 생성

2. 1번에서 생성한 Tilemap에 Tilemap Collider 2D Component, Tilemap Collider 2D Component, Composite Collider 2D를

    추가하고, Rigidbody 2D의 Type을 Static으로 변경해준다.

3. 2번에서 추가한 Tilemap Collider 2D Component의 Used By Composite를 체크하여 Composite에게 모양을 위임한다.

4. Palette에서 아무 타일을 선택하여 경계선을 만든다.

5. Tilemap Renderer Component의 Mask Interaction을 Visible Inside Mask로 변경하여 보이지 않도록 한다.

 

 

 

 

 

[ 섹션 13. 미니 RPG ]

 환경세팅

  - Terrain은 Unity에서 개발자가 직접 지형을 제작할 수 있도록 제공한 Tool이다.

  - Terrain은 [ Hierarchy ] - [ 오른쪽 마우스 ] - [ 3D Object ] - [ Terrain ] 을 통해 생성한다.

 

  - Light의 Type에는 Realtime, Mixed, Baked가 있다.

   > Realtime은 조명이 씬에 직접적인 빛을 제공하고 매 프레임마다 업데이트되어 게임 오브젝트가 씬 내에서 이동시

     조명은 즉시 업데이트된다. (상당한 부하를 유발)

   > Baked는 Lightmap을 Baking할 때 Scene의 Static Object에 대한 조명 효과가 계산되고 Texture에 기록된다.

     (즉, 움직이지 않는 물체의 빛을 미리 계산해두는 것)

   > 물체가 많아질수록 Baking 시간이 상당히 오래 걸린다. 따라서 개발 도중에 물체를 추가할때마다 자동으로 Baking

      하는 것을 막기 위해 [ Window ] - [ Rendering ] - [ Lighting Settings ] - [ Lightmapping Setting ] 의 Auto Generate의

      체크를 해제한다. (최신 버전에서 에러 발생시 Compress Lightmaps의 체크를 해제)

 

 이동

  - NavMesh는 Navigation Mesh의 줄임말로, 게임 월드에서 걸을 수 있는 표면을 뜻한다.

  - NavMesh는 [ Window ] - [ AI ] - [ Navigation ] 에서 설정 가능하다.

   > [ Bake ] 에서 미리 갈 수 있는 영역 또는 제한 사항들을 Bake한다.

     (계산에 포함되는 Mesh는 Navigation Static 속성을 가진다.)

  - 설정된 NavMesh를 이용하여 목적지까지 경로를 찾고 이동하는 캐릭터에 부착되는 컴포넌트가 Nav Mesh Agent이다.

NavMesh를 이용한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    //...
    
    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
        if (dir.magnitude < 0.1f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            _state = PlayerState.Idle;
        }
        else
        {
            // NavMesh를 사용
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            float moveDist = Mathf.Clamp(Time.deltaTime * _speed, 0, dir.magnitude);
            nma.Move(dir.normalized * moveDist);

            Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green); // Ray를 시각적으로 표현
            if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"))) // 건물, 사물과 인접시 Player가 멈추도록 설정
            {
                _state = PlayerState.Idle;
                return;
            }

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
        }

        // 애니매이션
        Animator anim = GetComponent<Animator>();
        anim.SetFloat("speed", _speed);
    }

    //...
}

 

 스탯

스탯 관리를 위한 공용 Stat Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Stat : MonoBehaviour
{
    [SerializeField]
    protected int _level;
    [SerializeField]
    protected int _hp;
    [SerializeField]
    protected int _maxHp;
    [SerializeField]
    protected int _attack;
    [SerializeField]
    protected int _defense;
    [SerializeField]
    protected float _moveSpeed;

    public int Level { get { return _level; } set { _level = value; } }
    public int Hp { get { return _hp; } set { _hp = value; } }
    public int MaxHp { get { return _maxHp; } set { _maxHp = value; } }
    public int Attack { get { return _attack; } set { _attack = value; } }
    public int Defense { get { return _defense; } set { _defense = value; } }
    public float MoveSpeed { get { return _moveSpeed; } set { _moveSpeed = value; } }

    private void Start()
    {
        _level = 1;
        _hp = 100;
        _maxHp = 100;
        _attack = 10;
        _defense = 5;
        _moveSpeed= 5.0f;
    }
}​

 

Player의 스탯 관리를 위해 Stat를 상속받는 PlayerStat Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStat : Stat
{
    [SerializeField]
    protected int _exp;
    [SerializeField]
    protected int _gold;

    public int Exp { get { return _exp; } set { _exp = value; } }
    public int Gold { get { return _gold; } set { _gold = value; } }

    private void Start()
    {
        _level = 1;
        _hp = 100;
        _maxHp = 100;
        _attack = 10;
        _defense = 5;
        _moveSpeed = 5.0f;
        _exp = 0;
        _gold = 0;
    }
}​

 

마우스 커서

  - Cursor로 사용하기 위한 Image는 Inspector 창에서 Texture Type을 Cursor로 설정한다. 

마우스 커서를 위한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    Texture2D _attackIcon;
    Texture2D _handIcon;

    enum CursorType 
    {
        None,
        Attack,
        Hand,
    }

    CursorType _cursorType = CursorType.None;

    void Start()
    {
        _attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack"); // Image 파일을 Texture2D를 통해 불러온다.
        _handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand"); // Image 파일을 Texture2D를 통해 불러온다.
    }
    void Update()
    {
        UpdateMouseCursor();
    }

    void UpdateMouseCursor()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }
    
    // ...
}

 

+ 추가 검색

 - Cursor.SetCursor() 함수는 Cursor의 Image를 사용자 지정 Texture로 바꿔준다.

  > 첫번째 인자는 Cursor에 지정할 Texture, 두번째 인자는 Cursor의 Spot, 세번째 인자는 최적화 방법이다.

 

 타게팅 락온

타게팅 락온을 위해 Define의 MouseEvent 추가
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    // ...

    public enum MouseEvent
    {
        Press, 
        PointerDown, // 마우스를 Click하는 순간
        PointerUp, // 마우스 Click을 떼는 순간
        Click, // 순간적인 Click (굉장히 짧은 시간)
    }

    // ...
}​

 

InputManager 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputManager
{
    // ...
    
    float _pressedTime = 0; // Click과 PointerUp을 구분하기 위한 것

    // 체크하는 부분이 유일해짐
    public void OnUpdate()
    {
        // ...

        if (MouseAction != null)
        {
            if (Input.GetMouseButton(0))
            {
                if (!_pressed) // 마우스를 Click하는 순간
                {
                    MouseAction.Invoke(Define.MouseEvent.PointerDown);
                    _pressedTime = Time.time;
                }
                MouseAction.Invoke(Define.MouseEvent.Press); // Press Event를 발생시켜 알려준다.
                _pressed = true;
            }
            else
            {
                if (_pressed)
                {
                    if (Time.time < _pressedTime + 0.2f) // 0.2초 내에 Mouse Click을 뗀 경우
                        MouseAction.Invoke(Define.MouseEvent.Click); // Click Event를 발생시켜 알려준다.
                    MouseAction.Invoke(Define.MouseEvent.PointerUp);
                } 
                _pressed = false;
                _pressedTime = 0; // 초기화
            }
        }
    }
}

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    void UpdateMouseCursor()
    {
        if (Input.GetMouseButton(0)) // 만약 마우스를 누르고 있다면 커서가 변경되면 안되기 때문
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }

    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    GameObject _lockTarget;
    void OnMouseEvent(Define.MouseEvent evt)
    {
        if (_state == PlayerState.Die)
            return;

        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        _state = PlayerState.Moving;

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if(_lockTarget != null)
                        _destPos = _lockTarget.transform.position;
                    else if (raycastHit)
                        _destPos = hit.point;
                }
                break;
            case Define.MouseEvent.PointerUp:
                _lockTarget = null;
                break;
        }
    }
}​

 

+ 추가 검색 (https://unity-beginner.tistory.com/17)

 - GetMouseButtonDown( )은 마우스 버튼을 클릭하고 있을때 계속해서 발생,

   GetMouseButton( )은 마우스 버튼을 클릭했을 때 한 번 발생,

   GetMouseButtonUp( )은 마우스 버튼을 놓았을 때 한 번 발생

  > 괄호안에 입력한 숫자가 0 : 마우스 왼쪽 버튼, 1 : 마우스 오른쪽 버튼, 2 : 마우스 휠 버튼에 해당한다.

 

 공격 #1

PlayerController에서 마우스 Cursor와 관련된 부분을 따로 빼 CursorController 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CursorController : MonoBehaviour
{
    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    Texture2D _attackIcon;
    Texture2D _handIcon;

    enum CursorType
    {
        None,
        Attack,
        Hand,
    }

    CursorType _cursorType = CursorType.None;

    void Start()
    {
        _attackIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Attack"); // Image 파일을 Texture2D를 통해 불러온다.
        _handIcon = Managers.Resource.Load<Texture2D>("Textures/Cursor/Hand"); // Image 파일을 Texture2D를 통해 불러온다.
    }

    void Update()
    {
        if (Input.GetMouseButton(0)) // 만약 마우스를 누르고 있다면 커서가 변경되면 안되기 때문
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, _mask))
        {
            if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
            {
                if (_cursorType != CursorType.Attack)
                {
                    Cursor.SetCursor(_attackIcon, new Vector2(_attackIcon.width / 5, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Attack;
                }
            }
            else
            {
                if (_cursorType != CursorType.Hand)
                {
                    Cursor.SetCursor(_handIcon, new Vector2(_attackIcon.width / 3, 0), CursorMode.Auto); // 마우스 Cursor를 설정한다.
                    _cursorType = CursorType.Hand;
                }
            }
        }
    }
}​

 

공격 Animation을 위한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    public enum PlayerState
    {
        Die,
        Moving,
        Idle,
        Skill,
    }

    [SerializeField]
    PlayerState _state = PlayerState.Idle;

    public PlayerState State // PlayerState가 변함과 동시에 Animation도 변경하기 위한 프로퍼티 생성
    {
        get { return _state; }
        set
        {
            _state = value;

            Animator anim = GetComponent<Animator>();
            switch (_state)
            {
                case PlayerState.Die:
                    anim.SetBool("attack", false);
                    break;
                case PlayerState.Idle:
                    anim.SetFloat("speed", 0);
                    anim.SetBool("attack", false);
                    break;
                case PlayerState.Moving:
                    anim.SetFloat("speed", _stat.MoveSpeed);
                    anim.SetBool("attack", false);
                    break;
                case PlayerState.Skill:
                    anim.SetBool("attack", true);
                    break;
            }
        }
    }

    // ...

    void UpdateMoving()
    {
        // 몬스터가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= 1)
            {
                State = PlayerState.Skill;
                return;
            }
        }

        // ...
    }

    // ...

    void OnHitEvent()
    {
        State = PlayerState.Moving;
    }

    void Update()
    {
        switch (State)
        {
            case PlayerState.Die:
                UpdateDie();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
            case PlayerState.Skill:
                UpdateSkill();
                break;
        }
    }

    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    GameObject _lockTarget;
    void OnMouseEvent(Define.MouseEvent evt)
    {
        if (_state == PlayerState.Die)
            return;

        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        State = PlayerState.Moving;

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if(_lockTarget != null)
                        _destPos = _lockTarget.transform.position;
                    else if (raycastHit)
                        _destPos = hit.point;
                }
                break;
        }
    }
}​

 

 공격 #2

  - Animation 전환시 반드시 Unity의 Tool을 사용하라는 법은 없다.

   > Parameter의 변경을 통한 Animation의 전환보다 Play( ), CrossFade( ) 사용이 더 편리할때도 있다.

   > CrossFade( )는 심지어 Blending도 가능하다. (두번째 매개변수를 통해)

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    public PlayerState State // PlayerState가 변함과 동시에 Animation도 변경하기 위한 프로퍼티 생성
    {
        get { return _state; }
        set
        {
            _state = value;

            Animator anim = GetComponent<Animator>();
            switch (_state)
            {
                case PlayerState.Die:
                    break;
                case PlayerState.Idle:
                    anim.CrossFade("WAIT", 0.1f);
                    break;
                case PlayerState.Moving:
                    anim.CrossFade("RUN", 0.1f);
                    break;
                case PlayerState.Skill:
                    anim.CrossFade("ATTACK", 0.1f);
                    break;
            }
        }
    }

    // ...

    void UpdateSkill()
    {
       if (_lockTarget != null)
        {
            Vector3 dir = _lockTarget.transform.position - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
            Quaternion quat = Quaternion.LookRotation(dir); // 목적지를 바라보기 위해서
            transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // 자연스럽게 연결되도록
        }
    }

    void OnHitEvent()
    {
        if (_stopSkill)
        {
            State = PlayerState.Idle;
        }
        else
        {
            State = PlayerState.Skill;
        }
    }

    // ...

    bool _stopSkill = false;
    void OnMouseEvent(Define.MouseEvent evt)
    {
        switch (State)
        {
            case PlayerState.Idle:
                OnMouseEvent_IdleRun(evt);
                break;
            case PlayerState.Moving:
                OnMouseEvent_IdleRun(evt);
                break;
            case PlayerState.Skill:
                {
                    if (evt == Define.MouseEvent.PointerUp)
                        _stopSkill = true;
                }
                break;
        }
    }

    void OnMouseEvent_IdleRun(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        State = PlayerState.Moving;
                        _stopSkill = false;

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if (_lockTarget == null && raycastHit)
                        _destPos = hit.point;
                }
                break;
            case Define.MouseEvent.PointerUp:
                _stopSkill = true;
                break;
        }
    }
}​

 

+ 추가 검색 (https://ansohxxn.github.io/unitydocs/anim/)

 - CrossFade( )는 Play( ) 에 대한 결과를 보다 더 부드럽고 자연스럽게 Blending 하여 Animation을 재생한다.

 > 첫번째 Parameter는 재생하고자 하는 Clip의 이름

 > 두번째 Parameter는 지연 시간 (다음 Animation으로 바뀌는데 걸리는 Fade 시간)

 > 세번째 Parameter는 Animation Layer

 > 네번째 Parameter는 Animation의 재생 시작 시점 (0 ~ 1 비율)

  ~> 0.0f로 설정시 다시 처음으로 돌아가 재생한다.

  ~> Clip의 Loop Time이 체크되어 있지 않더라도 반복 재생시킬 수 있다.

 

체력 게이지 #1

  - UI는 2D UI와 (게임 세상과 별개), 3D UI (게임 세상과 공존)로 나눌 수 있다.

   > 3D UI를 만들기 위해서는 Canvas의 Render Mode를 World Space로 변경한다.

   > 3D UI를 만들때 Canvas의 Scale을 약 1/100 로 줄이면 적당한 크기로 변경이 가능하다.

 

- Slider의 Value가 0과 1일때 텅 비어 있지도, 가득 차있지도 않다.

 > 이는 Fill Area의 [ Inspector ] - [ Rect Transform ] 에서 Left와 Right의 값을 전부 0으로 설정하면 해결이 가능하다.

설정 전의 모습
Fill Area의 Rect Transform의 Left와 Right의 값을 전부 0으로 설정
설정 후의 모습

 

- Slider의 Fill이 Background의 경계선을 넘어 튀어나와 있다.

 > 이는 Fill의 [ Inspector ] - [ Rect Transform ] 에서 Left와 Right의 값을 전부 0으로 설정하면 해결이 가능하다.

설정 전의 모습
Fill의 Rect Transform의 Left와 Right의 값을 전부 0으로 설정
설정 후의 모습

 

HP_Bar의 Canvas를 Prefab으로 저장한 뒤 UI_HPBar Script 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UI_HPBar : UI_Base
{
    enum GameObjects
    {
        HPBar
    }
    public override void Init()
    {
        Bind<GameObject>(typeof(GameObjects));
    }

    void Update()
    {
        // HP_Bar를 캐릭터의 머리 위로 위치시키기 위해서
        Transform parent = transform.parent;
        transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y); // 부모님 위치를 기준으로 Collider만큼의 높이를 올려 저장
    }
}​

 

UIManager 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UIManager
{
    // ...

    public T MakeWorldSpaceUI<T>(Transform parent = null, string name = null) where T : UI_Base
    {
        if (string.IsNullOrEmpty(name))
            name = typeof(T).Name;

        GameObject go = Managers.Resource.Instantiate($"UI/WorldSpace/{name}");

        if (parent != null)
            go.transform.SetParent(parent);

        Canvas canvas = go.GetOrAddComponent<Canvas>();
        canvas.renderMode = RenderMode.WorldSpace;
        canvas.worldCamera = Camera.main;

        return Util.GetOrAddComponent<T>(go);
    }

    // ...
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    void Start()
    {
        _stat = gameObject.GetComponent<PlayerStat>();

        Managers.Input.MouseAction -= OnMouseEvent; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.MouseAction += OnMouseEvent;

        Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    // ...
}​

 

 

 체력 게이지 #2

상속받는 Class들이 Start 함수 선언 없이 Init 함수를 실행하도록 UI_Base 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public abstract class UI_Base : MonoBehaviour
{
    // ...
    
    public abstract void Init();

    void Start()
    {
        Init();
    }

    // ...
}​

 

Billboard 기능 구현 및 실시간 HP 감소를 위한 UI_HPBar 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class UI_HPBar : UI_Base
{
    enum GameObjects
    {
        HPBar
    }

    Stat _stat;

    public override void Init()
    {
        Bind<GameObject>(typeof(GameObjects));
        _stat = transform.parent.GetComponent<Stat>();
    }

    void Update()
    {
        // HP_Bar를 캐릭터의 머리 위로 위치시키기 위해서
        Transform parent = transform.parent;
        transform.position = parent.position + Vector3.up * (parent.GetComponent<Collider>().bounds.size.y); // 부모님 위치를 기준으로 Collider만큼의 높이를 올려 저장
        transform.rotation = Camera.main.transform.rotation; // 자신이 바라보는 방향을 카메라가 바라보는 방향으로 설정 (Billboard의 개념)

        float ratio = _stat.Hp / (float)_stat.MaxHp;
        SetHpRatio(ratio);
    }

    public void SetHpRatio(float ratio)
    {
        GetObject((int)GameObjects.HPBar).GetComponent<Slider>().value = ratio;
    }
}

 

Hit Event 발생시 상대방의 HP를 감소시키기 위한 PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : MonoBehaviour
{
    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            Stat myStat = gameObject.GetComponent<PlayerStat>();
            int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
            Debug.Log(damage);
            targetStat.Hp -= damage;
        }

        if (_stopSkill)
        {
            State = PlayerState.Idle;
        }
        else
        {
            State = PlayerState.Skill;
        }
    }

    // ...
}​

 

+ 추가 검색

  - Unity에서 3D 오브젝트가 카메라를 바라보도록 만드는 기술을 Billboard라고 한다.

   > UI인 Slider를 카메라를 바라보도록 수정할 경우 좌우 반전이 된 채로 표시된다.

      ( transform.LookAt(Camera.main) 을 통해 )

    ~> 따라서 UI인 Slider에 Billboard 기술을 적용하기 위해서는 아래와 같이 수정한다.

         ( transform.rotation = Camera.main.transform.rotation 으로 수정 )

 몬스터 AI #1

  - 새로 생성하고자 하는 MonsterController가 PlayerController와 겹치는 부분이 상당히 많아서 이럴때는 BaseController를

    새로 생성하여 MonsterController와 PlayerController가 BaseController를 상속받도록 하는 것이 좋다.

Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum State
    {
        Die,
        Moving,
        Idle,
        Skill,
    }

    // ...
}​

 

Base Controller 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class BaseController : MonoBehaviour
{
    [SerializeField]
    protected Vector3 _destPos; // 목적지 좌표를 저장하기 위한 변수

    [SerializeField]
    protected Define.State _state = Define.State.Idle;

    [SerializeField]
    protected GameObject _lockTarget;

    public virtual Define.State State // State가 변함과 동시에 Animation도 변경하기 위한 프로퍼티 생성
    {
        get { return _state; }
        set
        {
            _state = value;

            Animator anim = GetComponent<Animator>();
            switch (_state)
            {
                case Define.State.Die:
                    break;
                case Define.State.Idle:
                    anim.CrossFade("WAIT", 0.1f);
                    break;
                case Define.State.Moving:
                    anim.CrossFade("RUN", 0.1f);
                    break;
                case Define.State.Skill:
                    anim.CrossFade("ATTACK", 0.1f);
                    break;
            }
        }
    }

    private void Start() 
    {
        Init();
    }

    void Update()
    {
        switch (State)
        {
            case Define.State.Die:
                UpdateDie();
                break;
            case Define.State.Moving:
                UpdateMoving();
                break;
            case Define.State.Idle:
                UpdateIdle();
                break;
            case Define.State.Skill:
                UpdateSkill();
                break;
        }
    }

    public abstract void Init();
    protected virtual void UpdateDie() { }
    protected virtual void UpdateMoving() { }
    protected virtual void UpdateIdle() { }
    protected virtual void UpdateSkill() { }
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    int _mask = (1 << (int)Define.Layer.Ground) | (1 << (int)Define.Layer.Monster);

    PlayerStat _stat;
    bool _stopSkill = false;

    public override void Init()
    {
        _stat = gameObject.GetComponent<PlayerStat>();

        Managers.Input.MouseAction -= OnMouseEvent; // 혹시라도 다른 곳에서 구독 신청을 하고 있는 경우를 대비
        Managers.Input.MouseAction += OnMouseEvent;

        if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateMoving()
    {
        // 몬스터가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= 1)
            {
                State = Define.State.Skill;
                return;
            }
        }

        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
        if (dir.magnitude < 0.1f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            State = Define.State.Idle;
        }
        else
        {
            // NavMesh를 사용
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            float moveDist = Mathf.Clamp(Time.deltaTime * _stat.MoveSpeed, 0, dir.magnitude);
            nma.Move(dir.normalized * moveDist);

            Debug.DrawRay(transform.position + Vector3.up * 0.5f, dir.normalized, Color.green); // Ray를 시각적으로 표현
            if (Physics.Raycast(transform.position + Vector3.up * 0.5f, dir, 1.0f, LayerMask.GetMask("Block"))) // 건물, 사물과 인접시 Player가 멈추도록 설정
            {
                if (Input.GetMouseButton(0) == false)
                    State = Define.State.Idle;
                return;
            }

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
        }
    }

    protected override void UpdateSkill()
    {
       if (_lockTarget != null)
        {
            Vector3 dir = _lockTarget.transform.position - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
            Quaternion quat = Quaternion.LookRotation(dir); // 목적지를 바라보기 위해서
            transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // 자연스럽게 연결되도록
        }
    }

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            Stat myStat = gameObject.GetComponent<PlayerStat>();
            int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
            Debug.Log(damage);
            targetStat.Hp -= damage;
        }

        if (_stopSkill)
        {
            State = Define.State.Idle;
        }
        else
        {
            State = Define.State.Skill;
        }
    }

    void OnMouseEvent(Define.MouseEvent evt)
    {
        switch (State)
        {
            case Define.State.Idle:
                OnMouseEvent_IdleRun(evt);
                break;
            case Define.State.Moving:
                OnMouseEvent_IdleRun(evt);
                break;
            case Define.State.Skill:
                {
                    if (evt == Define.MouseEvent.PointerUp)
                        _stopSkill = true;
                }
                break;
        }
    }

    void OnMouseEvent_IdleRun(Define.MouseEvent evt)
    {
        RaycastHit hit;
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        bool raycastHit = Physics.Raycast(ray, out hit, 100.0f, _mask);
        //Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f); // Ray를 시각적으로 표현

        switch (evt)
        {
            case Define.MouseEvent.PointerDown:
                {
                    if (raycastHit)
                    {
                        _destPos = hit.point;
                        State = Define.State.Moving;
                        _stopSkill = false;

                        if (hit.collider.gameObject.layer == (int)Define.Layer.Monster)
                            _lockTarget = hit.collider.gameObject;
                        else
                            _lockTarget = null;
                    }
                }
                break;
            case Define.MouseEvent.Press:
                {
                    if (_lockTarget == null && raycastHit)
                        _destPos = hit.point;
                }
                break;
            case Define.MouseEvent.PointerUp:
                _stopSkill = true;
                break;
        }
    }
}​

 

몬스터 AI #2

MonsterController 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : BaseController
{
    Stat _stat;

    [SerializeField]
    float _scanRange = 10;

    [SerializeField]
    float _attackRange = 2;

    public override void Init()
    {
        _stat = gameObject.GetComponent<Stat>();

        if (gameObject.GetComponentInChildren<UI_HPBar>() == null)
            Managers.UI.MakeWorldSpaceUI<UI_HPBar>(transform);
    }

    protected override void UpdateIdle()
    {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        if (player == null)
            return;

        float distance = (player.transform.position - transform.position).magnitude;
        if (distance <= _scanRange) // 사정 거리 안으로 들어올 경우 움직이기 시작
        {
            _lockTarget = player;
            State = Define.State.Moving;
            return;
        }
    }

    protected override void UpdateMoving()
    {
        // 플레이어가 내 사정거리보다 가까우면 공격
        if (_lockTarget != null)
        {
            _destPos = _lockTarget.transform.position;
            float distance = (_destPos - transform.position).magnitude;
            if (distance <= _attackRange)
            {
                NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
                nma.SetDestination(transform.position);
                State = Define.State.Skill;
                return;
            }
        }

        Vector3 dir = _destPos - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
        if (dir.magnitude < 0.1f) // 만약 목적지까지 거의 도착을 완료했다면
        {
            State = Define.State.Idle;
        }
        else
        {
            // NavMesh를 사용
            NavMeshAgent nma = gameObject.GetOrAddComponent<NavMeshAgent>();
            nma.SetDestination(_destPos);
            nma.speed = _stat.MoveSpeed;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime); // LookAt을 위한 회전을 보다 자연스럽게 하도록
        }
    }

    protected override void UpdateSkill()
    {
        if (_lockTarget != null)
        {
            Vector3 dir = _lockTarget.transform.position - transform.position; // 목적지까지의 방향벡터를 알 수 있다.
            Quaternion quat = Quaternion.LookRotation(dir); // 목적지를 바라보기 위해서
            transform.rotation = Quaternion.Lerp(transform.rotation, quat, 20 * Time.deltaTime); // 자연스럽게 연결되도록
        }
    }

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            Stat myStat = gameObject.GetComponent<Stat>();
            int damage = Mathf.Max(0, myStat.Attack - targetStat.Defense);
            targetStat.Hp -= damage;

            if (targetStat.Hp > 0)
            {
                float distance = (_lockTarget.transform.position - transform.position).magnitude;
                if (distance <= _attackRange)
                    State = Define.State.Skill;
                else
                    State = Define.State.Moving;
            }
            else
            {
                State = Define.State.Idle;
            }
        }
        else
        {
            State = Define.State.Idle;
        }
    }
}​

 

 Destroy #1

  - Destroy된 Object는 사실 실제로 삭제된건 C++ Native Object이고, UnityEngine.Object C# Class 부분은 아직 Memory

    상에 존재하기 때문에 실제로 null이 된 것은 아니지만 없어진 것처럼 행동해야 하기 때문에 "null"로 처리된다.

   > 이것이 바로 fake null 이다.

   > 이에 대한 처리가 UnityEngine.Object의 == 연산자 오버로딩에 구현되어 있다.

      (진짜 null이 된게 아님에도 Destroy된 Object == null 을 하면 true를 리턴하도록 구현됨)

  - Destroy된 Object의 Component를 참조하는 것들을 잘 확인해야 한다.

   > Object가 해제되면 당연히 해당 Object의 Component도 참조할 수 없다.

MosterController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : BaseController
{
    // ...

    void OnHitEvent()
    {
        // ...

            if (targetStat.Hp <=0)
            {
                GameObject.Destroy(targetStat.gameObject);
            }

        // ...
    }
}​

 

CameraController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    // ...

    // Player의 움직임이 실행된 뒤 Camera의 위치를 움직여야 떨리는 현상이 줄어든다.
    void LateUpdate() // 게임 Logic에서 LateUpdate()는 Update()보다 늦게 실행된다.
    {
        if (_mode == Define.CameraMode.QuaterView)
        {
            if (_player == null)
            {              
                return;
            }

            // ...
        }
    }

    // ...
}​

 

+ 추가 검색 (https://ansohxxn.github.io/unitydocs/fakenull/)

  - 해결 방법 1 : Object를 bool로 묵시적 형변환

  - 해결 방법 2 : Destroy 후 진짜 null을 대입

  - 해결 방법 3 : System.Object로 형변환 후 비교

 

 Destroy #2

Define 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Define
{
    public enum WorldObject
    {
        Unknown,
        Player,
        Monster,
    }
    
    // ...
}​

 

BaseController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class BaseController : MonoBehaviour
{
    // ...

    public Define.WorldObject WorldObjectType { get; protected set; } = Define.WorldObject.Unknown;

    // ...
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    // ...

    public override void Init()
    {
        WorldObjectType = Define.WorldObject.Player;

        // ...
    }

    // ...
}​

 

MonsterController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class MonsterController : BaseController
{
    // ...

    public override void Init()
    {
        WorldObjectType = Define.WorldObject.Monster;

        // ...
    }

    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            // ...

            if (targetStat.Hp <=0)
            {
                Managers.Game.Despawn(targetStat.gameObject);
            }

            // ...
        }
        // ...
    }
}​

 

GameManagerEx 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManagerEx : MonoBehaviour
{
    // 스폰된 Object들은 ID로 관리하는 것이 좋다 => Dictionary가 적당
    // 서버에서는 해당 ID를 통해 통신하기 때문이다.
    //Dictionary<int, GameObject> _players = new Dictionary<int, GameObject>();
    //Dictionary<int, GameObject> _monsters = new Dictionary<int, GameObject>();
   
    // 현재는 멀티가 아닌 싱글 게임이기 때문에 아래와 같이 관리한다.
    GameObject _player;
    HashSet<GameObject> _monsters = new HashSet<GameObject>(); // HashSet은 Dictionary와 비슷하지만 Key가 X

    public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
    {
        GameObject go = Managers.Resource.Instantiate(path, parent);

        switch (type)
        {
            case Define.WorldObject.Monster:
                _monsters.Add(go);
                break;
            case Define.WorldObject.Player:
                _player = go;
                break;
        }

        return go;
    }

    public Define.WorldObject GetWorldObjectType(GameObject go)
    {
        BaseController bc = go.GetComponent<BaseController>();
        if (bc == null)
            return Define.WorldObject.Unknown;
        return bc.WorldObjectType ;
    }

    public void Despawn(GameObject go) 
    {
        Define.WorldObject type = GetWorldObjectType(go);

        switch (type)
        {
            case Define.WorldObject.Monster:
                {
                    if (_monsters.Contains(go))
                        _monsters.Remove(go);
                }
                break;
            case Define.WorldObject.Player:
                {
                if (_player == go)
                    _player = null;      
                }
                break;
        }

        Managers.Resource.Destroy(go);
    }
}​

 

Managers 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Managers : MonoBehaviour
{
    // ...
    
    GameManagerEx _game = new GameManagerEx();

    // ...

    public static GameManagerEx Game { get { return Instance._game; } }

    // ...
}​

 

Extension 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public static class Extension
{
    // ...

    // 폴링하는 Object일 경우 Destroy가 아닌 비활성화 처리가 된다.
    // 따라서 null 체크뿐만이 아닌 활성화 상태 여부도 체크해줘야 한다.
    public static bool IsValid(this GameObject go) 
    {
        return go != null && go.activeSelf;
    }
}​

 

CameraController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraController : MonoBehaviour
{
    // ...

    public void SetPlayer(GameObject player) { _player = player; }

    // Player의 움직임이 실행된 뒤 Camera의 위치를 움직여야 떨리는 현상이 줄어든다.
    void LateUpdate() // 게임 Logic에서 LateUpdate()는 Update()보다 늦게 실행된다.
    {
        if (_mode == Define.CameraMode.QuaterView)
        {
            if (_player.IsValid() == false)
            {              
                return;
            }

            // ...
        }
    }

    // ...
}​

 

GameScene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameScene : BaseScene
{
    protected override void Init() {
        // ...

        GameObject player = Managers.Game.Spawn(Define.WorldObject.Player, "Player");
        Camera.main.gameObject.GetOrAddComponent<CameraController>().SetPlayer(player);
        Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
    }

    // ...
}​

 

+ 추가 검색 (https://wlsdn629.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-Dictionary-HashTable-HastSet-%EA%B0%84%EB%8B%A8-%EC%84%A4%EB%AA%85)

  - HashSet은 Key가 존재하지 않으며 Value(들)의 구성으로 이루어져 있다.

   > 순서가 존재하지 않는다.

   > 각 항목의 중복을 허용하지 않는다. (즉, Value 중복을 허용 X)

   > Add( ), Remove( ), Clear( ), UnionWith( ), IntersectWith( ), ExceptWith( ) 등의 함수가 존재한다.

 

 레벨업

  - Damage와 관련된 부분은 공격을 하는 객체가 아닌 공격을 받는 객체에서 처리해주는 것이 좋다.

   (공격을 받는 객체의 방어력, 버프 등이 존재하기 때문)

Stat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Stat : MonoBehaviour
{
    // ...

    public virtual void OnAttacked(Stat attacker) 
    {
        int damage = Mathf.Max(0, attacker.Attack - Defense);
        Hp -= damage;
        if (Hp <= 0) {
            Hp = 0;
            OnDead();
        }
    }

    protected virtual void OnDead() 
    {
        Managers.Game.Despawn(gameObject);
    }
}​

 

PlayerStat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStat : Stat
{
    // ...

    protected override void OnDead() 
    {
        Debug.Log("Player Dead");
    }
}​

 

MonsterController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            targetStat.OnAttacked(_stat);
        }

        // ...
    }

    // ...
}​

 

PlayerController 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class PlayerController : BaseController
{
    // ...

    void OnHitEvent()
    {
        if (_lockTarget != null)
        {
            Stat targetStat = _lockTarget.GetComponent<Stat>();
            targetStat.OnAttacked(_stat);
        }

        // ...
    }

    // ...
}​

 

  - Monster를 죽일 경우 Monster에 따른 경험치 휙득량을 데이터를 통해 관리해주는 것이 좋다.

  - Level Up 확인 코드는 유일한 것이 좋다. (그렇지 않으면 고된 복붙 작업)

StatData json 파일 수정
{
    "stats": [
        {
            "level": "1",
            "maxHp": "200",
            "attack": "20",
            "totalExp": "0"
        },
        {
            "level": "2",
            "maxHp": "250",
            "attack": "25",
            "totalExp": "10"
        },
        {
            "level": "3",
            "maxHp": "300",
            "attack": "30",
            "totalExp": "20"
        }
    ]
}​

 

Data.Contents 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Data
{
    #region Stat
    [Serializable]
    public class Stat
    {
        public int level;
        public int maxHp;
        public int attack;
        public int totalExp;
    }

    // ...
    #endregion
}​

 

Stat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Stat : MonoBehaviour
{
    // ...

    public virtual void OnAttacked(Stat attacker) 
    {
        int damage = Mathf.Max(0, attacker.Attack - Defense);
        Hp -= damage;
        if (Hp <= 0) {
            Hp = 0;
            OnDead(attacker);
        }
    }

    protected virtual void OnDead(Stat attacker) 
    {
        PlayerStat playerStat = attacker as PlayerStat;
        if (playerStat != null)
        {
            playerStat.Exp += 5;
        }

        Managers.Game.Despawn(gameObject);
    }
}​

 

PlayerStat 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerStat : Stat
{
    // ...

    public int Exp { 
        get { return _exp; } 
        set 
        { 
            _exp = value;

            // Level Up 체크
            int level = Level;
            while (true)
            {
                Data.Stat stat;
                if (Managers.Data.StatDict.TryGetValue(level + 1, out stat) == false) // 다음 Level이 존재하지 않을 경우
                    break;
                if (_exp < stat.totalExp)
                    break;
                level++;
            }

            if (level != Level) // Level에 변화가 있을 경우
            {
                Level = level;
                SetStat(Level);
            }
        } 
    }

    // ...

    private void Start()
    {
        _level = 1;
        _exp = 0;
        _defense = 5;
        _moveSpeed = 5.0f;
        _gold = 0;

        SetStat(_level);
    }

    public void SetStat(int level)
    {
        Dictionary<int, Data.Stat> dict = Managers.Data.StatDict;
        Data.Stat stat = dict[level];
        _hp = stat.maxHp;
        _maxHp = stat.maxHp;
        _attack = stat.attack;
    }

    protected override void OnDead(Stat attacker) 
    {
        Debug.Log("Player Dead");
    }
}

 

몬스터 자동 생성

SpawningPool 생성
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class SpawningPool : MonoBehaviour
{
    [SerializeField]
    int _monsterCount = 0; // 현재 몬스터 수
    int _reserveCount = 0; // 현재 Spawn 예약 수

    [SerializeField]
    int _keepMonsterCount = 0; // 유지시켜야 하는 몬스터 수

    [SerializeField]
    Vector3 _spawnPos; // Spawn 중심점

    [SerializeField]
    float _spawnRadius = 15.0f; // Spawn 중심점을 기준으로 랜덤 생성을 위한 생성 거리 범위 제한

    [SerializeField]
    float _spawnTime = 5.0f; // 랜덤 생성을 위한 생성 시간 범위 제한

    public void AddMonsterCount(int value) { _monsterCount += value; }
    public void SetkeepMonsterCount(int count) { _keepMonsterCount = count; }

    void Start()
    {
        Managers.Game.OnSpawnEvent -= AddMonsterCount;
        Managers.Game.OnSpawnEvent += AddMonsterCount;
    }

    void Update()
    {
        while (_reserveCount + _monsterCount < _keepMonsterCount)
        {
            StartCoroutine("ReserveSpawn");
        }
    }

    IEnumerator ReserveSpawn()
    {
        _reserveCount++;

        yield return new WaitForSeconds(Random.Range(0, _spawnTime));
        GameObject obj = Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");

        NavMeshAgent nma = obj.GetOrAddComponent<NavMeshAgent>();
        Vector3 randPos;
        while(true) // randPos가 갈 수 있는 길이 나올때까지 반복
        {
            Vector3 randDir = Random.insideUnitSphere * Random.Range(0, _spawnRadius); // 랜덤으로 뽑힌 방향벡터가 randDir에 담긴다. (insideUnitCircle은 2D)
            randDir.y = 0; // 땅을 뜷고 Spawn 되지 않도록
            randPos = _spawnPos + randDir;

            // randPos가 갈 수 있는 길인지 체크
            NavMeshPath path = new NavMeshPath();
            if (nma.CalculatePath(randPos, path)) // 갈 수 있는 길이면 True, 갈 수 없는 길이면 False를 반환
                break;
        }
        obj.transform.position = randPos;

        _reserveCount--;
    }
}​

 

GameManagerEx 수정
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManagerEx : MonoBehaviour
{
    // ...
    
    public Action<int> OnSpawnEvent; // int는 증가/감소 수

    // ...

    public GameObject Spawn(Define.WorldObject type, string path, Transform parent = null)
    {
        // ...

        switch (type)
        {
            case Define.WorldObject.Monster:
                // ...
                if (OnSpawnEvent != null)
                    OnSpawnEvent.Invoke(1);
                break;
            // ...
        }

        // ...
    }

    // ...
    
    public void Despawn(GameObject go) 
    {
        // ...

        switch (type)
        {
            case Define.WorldObject.Monster:
                {
                    if (_monsters.Contains(go)) {
                        _monsters.Remove(go);
                        if (OnSpawnEvent != null)
                            OnSpawnEvent.Invoke(-1);
                    } 
                }
                break;
            // ...
        }

        // ...
    }
}

 

GameScene 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameScene : BaseScene
{
    protected override void Init() {
        // ...

        //Managers.Game.Spawn(Define.WorldObject.Monster, "Knight");
        GameObject go = new GameObject { name = "SpawningPool" };
        SpawningPool pool = go.GetOrAddComponent<SpawningPool>();
        pool.SetkeepMonsterCount(5);
    }

    // ...
}​

 

+ 추가 검색 (  )

  - Unity는 난수 생성과 관련된 함수들이 있는 집합인 Random 이라는 기능을 제공한다.

   ~> 가장 대표적인 기능이 Random.Range(min, max) 이며 이는 min과 max 범위 내에서 랜덤한 값을 반환한다.

        (이때 max의 Type이 int일 경우 max의 값은 포함되지 X)

   ~> Random.insideUnitSphere은 반경 1을 갖는 구 안의 임의의 위치(Vector3)를 반환하는 프로퍼티이다.

   ~> Random. insideUnitCircle 은 반경 1을 갖는 구 안의 임의의 위치(Vector2)를 반환하는 프로퍼티이다.

 

 

+ Recent posts