Project details - paquantum/2d-game-project GitHub Wiki

ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

  • ํ•ด๋‹น ํ”„๋กœ์ ํŠธ๋Š” ๊ฒŒ์ž„ ์„œ๋ฒ„ ๊ฐœ๋ฐœ์„ ์ฃผ์š” ๋ชฉ์ ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•˜๊ณ  ๋™์ž‘์„ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด Unity ์—”์ง„์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ํ”„๋กœ์ ํŠธ๋Š” 2D Top down MMORPG ์žฅ๋ฅด์˜ ๊ฒŒ์ž„์œผ๋กœ '๋ฐ”๋žŒ์˜ ๋‚˜๋ผ' ์ผ๋ถ€๋ฅผ ๋ฒค์น˜๋งˆํ‚น ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • Windows IOCP ์„œ๋ฒ„๋กœ ์—ฐ๊ฒฐ์€ TCP/IP, ์ง๋ ฌํ™” ๋„๊ตฌ๋Š” Flatbuffers๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.


Sever

ํ•™์Šต

1. ๋ฉ€ํ‹ฐ์“ฐ๋ ˆ๋“œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ

  • ์›์ž์  ์—ฐ์‚ฐ์„ ์œ„ํ•ด atomic๊ณผ compare_exchange_strong() ํ•จ์ˆ˜ ๋“ฑ ํ•™์Šต.
  • ์“ฐ๋ ˆ๋“œ ๋งˆ๋‹ค ์ž์‹ ์˜ ์ €์žฅ์†Œ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” Thread Local Storage ํ•™์Šต.
  • ThreadManager ํด๋ž˜์Šค๋กœ TLS ์ดˆ๊ธฐํ™” ๋ฐ ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„์„œ ์‹คํ–‰ ํ•™์Šต.
  • ์ฝ๊ธฐ์™€ ์“ฐ๊ธฐ ์ž‘์—…์— ๋”ฐ๋ผ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก Reader-Writer Lock์„ SpinLock ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  RAII๋ฅผ ์‚ฌ์šฉํ•œ Lock ํด๋ž˜์Šค ๊ตฌํ˜„ ํ•™์Šต.

2. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ

  • ํŠน์ • ๊ฐ์ฒด๊ฐ€ ์ฐธ์กฐ๋˜๊ณ  ์žˆ์Œ์—๋„, ์‚ญ์ œํ•˜๋ฉด์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ Reference Counting ๊ฐœ๋… ํ•™์Šต.
  • ์Šค๋งˆํŠธ ํฌ์ธํ„ฐ ํ•™์Šต.
  • ๊ธฐ๋ณธ new, delete ์—ฐ์‚ฐ์ž ๋Œ€์‹ ์— ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•œ Allocator๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํ•™์Šต.
  • ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์‚ญ์ œํ•˜๊ณ  ์ ‘๊ทผ๋„ ๋ง‰์Œ์œผ๋กœ์จ ์˜ค์—ผ๋œ ๋ฉ”๋ชจ๋ฆฌ์— ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•˜๋Š” ๊ฒƒ์œผ๋กœ, ๊ฐœ๋ฐœ ๋‹จ๊ณ„์—์„œ ์œ ์šฉํ•œ Stomp ๋ฐฉ์‹ Allocator์„ ํ•™์Šต.
  • STL์—์„œ๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ new, delete ๋Œ€์‹ ์— ์‚ฌ์šฉํ•˜๋„๋ก STL Allocator ํ•™์Šต.
  • ๋ฉ”๋ชจ๋ฆฌ ํ’€๊ณผ ์˜ค๋ธŒ์ ํŠธ ํ’€ ํ•™์Šต.

3. ๋„คํŠธ์›Œํฌ ํ”„๋กœ๊ทธ๋ž˜๋ฐ

  • ์†Œ์ผ“ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ธฐ๋ณธ ํ•™์Šต.
  • TCP์™€ UDP์˜ ํŠน์ง• ๋ฐ ์—ฐ๊ฒฐ ํ•™์Šต.
  • ๋‹ค์–‘ํ•œ ์†Œ์ผ“ ์˜ต์…˜์— ๋Œ€ํ•œ ํ•™์Šต.
  • ::ioctlsocket() ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ ๋…ผ๋ธ”๋กœํ‚น ์†Œ์ผ“ ํ•™์Šต.
  • ๋‹ค์–‘ํ•œ ์†Œ์ผ“ ๋ชจ๋ธ ํ•™์Šต.(select, WSAEventSelect, Overlapped(์ด๋ฒคํŠธ/์ฝœ๋ฐฑ), Completion Port)

1. ๊ตฌํ˜„ ์†Œ๊ฐœ


2. ์ฝ”๋“œ ์†Œ์Šค ์ผ๋ถ€ ์†Œ๊ฐœ



Client

1. ๊ตฌํ˜„ ์†Œ๊ฐœ

UI

  • ํšŒ์›๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ
  • ์บ๋ฆญํ„ฐ ์ƒ์„ฑ ๋ฐ ์บ๋ฆญํ„ฐ ์„ ํƒ
  • ์ธ๋ฒคํ† ๋ฆฌ ํ™”๋ฉด ๋ฐ ์บ๋ฆญํ„ฐ ์ •๋ณด
  • ์ฑ„ํŒ…
  • NPC์™€ ์ƒํ˜ธ์ž‘์šฉ
  • ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด ์ •๋ณด
  • ๊ฑฐ๋ž˜

๊ธฐ๋Šฅ

  • ํƒ€์ผ๋งต์„ ์‚ฌ์šฉํ•ด์„œ ๋งต ์ œ์ž‘
  • ์บ๋ฆญํ„ฐ ์ด๋™ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธฐ๋Šฅ
  • NPC์—๊ฒŒ ๋ฌผ๊ฑด์„ ์‚ฌ๊ณ  ํŒŒ๋Š” ๊ธฐ๋Šฅ
  • ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด ์ƒ์„ฑ ๋ฐ ์ด๋™ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธฐ๋Šฅ
  • ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด ํด๋ฆญ ์ด๋ฒคํŠธ ๊ธฐ๋Šฅ(์ •๋ณด ๋ณด๊ธฐ, ๊ฑฐ๋ž˜ ์š”์ฒญ ๋ฒ„ํŠผ ๋“ฑ)
  • ์ธ๋ฒคํ† ๋ฆฌ, ๊ฑฐ๋ž˜, NPC ์ƒํ˜ธ์ž‘์šฉ์— ์‚ฌ์šฉ๋˜๋Š” ์•„์ดํ…œ ๋ฆฌ์ŠคํŠธ ๋“ฑ์„ ์ƒ์„ฑ ๋ฐ ์„ ํƒํ•˜๋Š” ๊ธฐ๋Šฅ
  • ์„ ํƒ ์‹œ, UX๋ฅผ ์œ„ํ•ด ์„ ํƒ ๋œ ์˜ค๋ธŒ์ ํŠธ ์ƒ‰์ƒ ๋ณ€๊ฒฝ ๊ธฐ๋Šฅ

2. ์†Œ์Šค ์ฝ”๋“œ ์ผ๋ถ€ ์†Œ๊ฐœ

NetworkManager

private void OnReceiveData(IAsyncResult ar)
{
    int bytesRead = stream.EndRead(ar);
    if (bytesRead <= 0) return;

    byte[] receivedData = new byte[bytesRead];
    Buffer.BlockCopy(receiveBuffer, 0, receivedData, 0, bytesRead);

    PacketHandler.HandlePacket(receivedData);
    stream.BeginRead(receiveBuffer, 0, receiveBuffer.Length, new AsyncCallback(OnReceiveData), null);
}

public void SendPacket(FlatBufferBuilder builder, short pktId)
{
    ...

    byte[] packetData = builder.SizedByteArray();
    // ํŒจํ‚ท ํ—ค๋” ์ƒ์„ฑ
    PacketHeader header;
    header.size = (short)(Marshal.SizeOf(typeof(PacketHeader)) + packetData.Length);
    header.id = pktId;

    // ํŒจํ‚ท ์ง๋ ฌํ™”
    byte[] headerBytes = Serialize(header);
    byte[] finalPacket = new byte[headerBytes.Length + packetData.Length];

    Buffer.BlockCopy(headerBytes, 0, finalPacket, 0, headerBytes.Length);
    Buffer.BlockCopy(packetData, 0, finalPacket, headerBytes.Length, packetData.Length);

    try
    {
        stream.Write(finalPacket, 0, finalPacket.Length);
    }
    catch (Exception ex)
    {
        Debug.LogError($"ํŒจํ‚ท ์ „์†ก ์‹คํŒจ: {ex.Message}");
    }
}
  • ์„œ๋ฒ„์™€ ๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ํด๋ž˜์Šค ์ผ๋ถ€ ๋‚ด์šฉ
  • ๋น„๋™๊ธฐ ์ฝ๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด NetworkStream ํด๋ž˜์Šค ์‚ฌ์šฉ
  • ๋ฐ›์€ ํŒจํ‚ท์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์ „์šฉ ํด๋ž˜์Šค PacketHandler ํ•จ์ˆ˜ ํ˜ธ์ถœ
  • SendPacket()์€ ํŒจํ‚ท์„ ์„œ๋ฒ„๋กœ ๋ณด๋‚ด๋Š” ์—ญํ• 
  • ํŒจํ‚ท ID์™€ ํฌ๊ธฐ๋ฅผ ์•Œ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌ์กฐ์ฒด๋กœ ์ •์˜๋œ PacketHeader๋ฅผ ๋ฒ„ํผ์— ๋„ฃ์Œ

PacketHandler

// ํŒจํ‚ท ํ—ค๋” ๊ตฌ์กฐ์ฒด ์ •์˜
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PacketHeader
{
    public short size; // ํŒจํ‚ท ์ „์ฒด ํฌ๊ธฐ
    public short id;   // ํŒจํ‚ท ID
}

public enum PKT_ID : short
{
    PKT_C_REGISTER = 1000, // C -> S
    PKT_S_TRADE_INVITATION = 1021
    ...
}

public class PacketHandler
{
    private static Dictionary<PKT_ID, Func<byte[], bool>> packetHandlers = new Dictionary<PKT_ID, Func<byte[], bool>>();

    public static void Init()
    {
        packetHandlers.Add(0, PacketHandler.Handle_INVALID);
        packetHandlers.Add(PKT_ID.PKT_S_REGISTER, PacketHandler.Handle_S_REGISTER);
        packetHandlers.Add(PKT_ID.PKT_S_TRADE_INVITATION, PacketHandler.Handle_S_TRADE_INVITATION);
        ...
    }

    public static bool Handle_INVALID(byte[] data)
    {
        Debug.Log("์ž˜๋ชป๋œ ํŒจํ‚ท ๋ฒˆํ˜ธ์ž…๋‹ˆ๋‹ค");
        return false;
    }

    public static bool Handle_S_REGISTER(byte[] data)
    {
        ByteBuffer bb = new ByteBuffer(data);
        S_REGISTER sRegister = S_REGISTER.GetRootAsS_REGISTER(bb);
        bool response = sRegister.Success;
        string msg = sRegister.Message;

        GameManager.Instance.RegisterProcess(msg, response);

        return true;
    }

    // ํŒจํ‚ท ๋ฐ›๋Š” ์—ญํ• 
    public static bool HandlePacket(byte[] data)
    {
        PacketHeader header = Deserialize<PacketHeader>(data);
        int headerSize = Marshal.SizeOf<PacketHeader>();
        byte[] payload = new byte[data.Length - headerSize];
        Buffer.BlockCopy(data, headerSize, payload, 0, payload.Length);
        
        if (!packetHandlers.TryGetValue((PKT_ID)header.id, out Func<byte[], bool> handler))
        {
            Debug.LogWarning("์•Œ ์ˆ˜ ์—†๋Š” ํŒจํ‚ท ID: " + header.id);
            handler = Handle_INVALID; // ๊ธฐ๋ณธ ํ•ธ๋“ค๋Ÿฌ ์‹คํ–‰
        }

        return handler.Invoke(payload);
    }
  • ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ ํŒจํ‚ท์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค๋กœ Init() ํ•จ์ˆ˜์—์„œ Dictionary ์ž๋ฃŒ๊ตฌ์กฐ์— ํŒจํ‚ทID์™€ ์‹คํ–‰ํ•  ํ•จ์ˆ˜ ์ €์žฅ
  • ์ž…๋ ฅ ๋ฐ›์€ ๋ฐ์ดํ„ฐ๋Š” NetworkManager์—์„œ ํ•ด๋‹น ํด๋ž˜์Šค์˜ HandlePacket์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌ
  • ํ—ค๋”๋ฅผ ๋ถ„์„ํ•ด์„œ ํ—ค๋”๋ฅผ ์ œ์™ธํ•œ ๋ฐ์ดํ„ฐ๋ฅผ payload์— ๋‹ด๊ณ  ํ—ค๋”์—์„œ ํŒจํ‚ทID๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ
  • ๋งŒ์•ฝ, ํŒจํ‚ท์ด ํšŒ์›๊ฐ€์ž… ํ•˜๋Š” PKT_S_REGISTER๋ผ๋ฉด Handle_S_REGISTER() ํ•จ์ˆ˜ ํ˜ธ์ถœ
  • ์„œ๋ฒ„์™€ ํ†ต์‹ ์€ Flatbuffers๋ฅผ ์‚ฌ์šฉ

ItemData - ์•„์ดํ…œ ๊ด€๋ฆฌ

public enum ItemType
{
    Weapon,     // ๋ฌด๊ธฐ
    Armor,      // ๋ฐฉ์–ด๊ตฌ
    Consumable, // ์†Œ๋น„ ์•„์ดํ…œ (์˜ˆ: ๋ฌผ์•ฝ)
    Accessory,  // ์•…์„ธ์„œ๋ฆฌ
    general,    // ์ผ๋ฐ˜
}

[CreateAssetMenu(fileName = "ItemData", menuName = "Scriptable Objects/ItemData")]
public class ItemData : ScriptableObject
{
    [Header("๊ธฐ๋ณธ ์ •๋ณด")]
    public int itemId;             // ์•„์ดํ…œ ๊ณ ์œ  ID (๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์—ฐ๋™ํ•  ๊ฒฝ์šฐ)
    public string itemName;        // ์•„์ดํ…œ ์ด๋ฆ„
    public ItemType type;          // ์•„์ดํ…œ ์ข…๋ฅ˜

    [Header("์„ค๋ช…")]
    [TextArea]
    public string description;     // ์•„์ดํ…œ ์„ค๋ช…

    [Header("๋Šฅ๋ ฅ์น˜/ํšจ๊ณผ")]
    public int atk;                // ๊ณต๊ฒฉ๋ ฅ (๋ฌด๊ธฐ์ผ ๊ฒฝ์šฐ)
    public int def;                // ๋ฐฉ์–ด๋ ฅ (๋ฐฉ์–ด๊ตฌ์ผ ๊ฒฝ์šฐ)
    public int healRecovery;       // ํšŒ๋ณต๋Ÿ‰ (์†Œ๋น„ ์•„์ดํ…œ, ์˜ˆ: ๋ฌผ์•ฝ)

    [Header("๊ฐ€๊ฒฉ")]
    public int price;              // ์•„์ดํ…œ ๊ฐ€๊ฒฉ

    [Header("UI")]
    public Sprite icon;            // UI์— ํ‘œ์‹œํ•  ์•„์ด์ฝ˜ (์˜ต์…˜)
}
  • ์•„์ดํ…œ์€ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ช‡๊ฐœ๋งŒ ์ถ”๊ฐ€ํ•  ๊ฒƒ์ด๋ฏ€๋กœ, ScriptableObject๋ฅผ ์ƒ์† ๋ฐ›์•„ Resources ํด๋”์—์„œ ๊ด€๋ฆฌ

GameManager

private static readonly Queue<Action> executionQueue = new Queue<Action>();

private void Awake()
{
    if (Instance == null)
    {
        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
    else
    {
        Destroy(gameObject);
    }
}

private void Update()
{
    while (true)
    {
        Action action = null;
        lock (executionQueue)
        {
            if (executionQueue.Count > 0)
                action = executionQueue.Dequeue();
            else
                break;
        }
        try
        {
            action?.Invoke();
        }
        catch (Exception ex)
        {
            Debug.LogError("๋ฉ”์ธ ์Šค๋ ˆ๋“œ ์ž‘์—… ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: " + ex);
        }
    }
}

public static void Enqueue(Action action)
{
    if (action == null)
        return;

    lock (executionQueue)
    {
        executionQueue.Enqueue(action);
    }
}
  • GameManager๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ํ•„์š”ํ•œ ์ž‘์—…์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ž‘์—… ํ ์ƒ์„ฑ
  • ๋‹ค๋ฅธ ํด๋ž˜์Šค์—์„œ UI ๋ณ€๊ฒฝ ์ž‘์—… ๋“ฑ ์š”์ฒญ

ItemSlotUI

public class ItemSlotUI : MonoBehaviour, IPointerClickHandler
{
    public int itemId;
    public int quantity;
    public TextMeshProUGUI itemNameText;

    // ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•œ ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ
    public System.Action<int> OnItemClicked;
    public System.Action<int, int> OnItemIdQuantityClicked;

    // ํ”„๋ฆฌํŒน์„ ์ดˆ๊ธฐํ™”ํ•  ๋•Œ ํ˜ธ์ถœํ•˜๋Š” ํ•จ์ˆ˜
    public void Setup(ItemData itemData, int quantity, int price)
    {
        ...
    }

    // IPointerClickHandler ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„ โ€“ ๋งˆ์šฐ์Šค ํด๋ฆญ ๊ฐ์ง€
    public void OnPointerClick(PointerEventData eventData)
    {
        OnItemClicked?.Invoke(itemId);
        OnItemIdQuantityClicked?.Invoke(itemId, quantity);
    }
}
  • ์•„์ดํ…œ ํ”„๋ฆฌํŒน์„ ์ƒ์„ฑํ•  ๋•Œ ํ”„๋ฆฌํŒน์— ํฌํ•จ๋œ ์Šคํฌ๋ฆฝํŠธ๋กœ์จ, ํด๋ฆญ ๊ฐ์ง€๋ฅผ ์œ„ํ•ด IPointerClickHandler๋ฅผ ์ƒ์†๋ฐ›๊ณ  OnPointerClick() ํ•จ์ˆ˜ ์ •์˜

์„œ๋ฒ„๋กœ ํŒจํ‚ท ๋ณด๋‚ด๋Š” ํ•จ์ˆ˜ ์˜ˆ์‹œ

private void OnTradeButtonClicked()
{
    // ์ƒ๋Œ€๋ฐฉ์—๊ฒŒ ๊ฑฐ๋ž˜ ์š”์ฒญ ์‘๋‹ต ๋ณด๋‚ด๊ธฐ...
    FlatBufferBuilder builder = new FlatBufferBuilder(1024);
    C_TRADE_REQUEST.StartC_TRADE_REQUEST(builder);
    C_TRADE_REQUEST.AddSenderId(builder, GameManager.Instance.currentPlayer.objectId);
    C_TRADE_REQUEST.AddReceiverId(builder, otherPlayerObjectId);
    var cTradeRequestPacket = C_TRADE_REQUEST.EndC_TRADE_REQUEST(builder);
    builder.Finish(cTradeRequestPacket.Value);
    
    PacketHandler.SendPacket(builder, (short)PKT_ID.PKT_C_TRADE_REQUEST);
}
  • ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ํŠน์ • ํ–‰๋™์„ ํ•˜๋ฉด ๊ทธ์— ๋งž๋Š” ํŒจํ‚ท์„ ์ƒ์„ฑํ•ด์„œ ์„œ๋ฒ„๋กœ ํŒจํ‚ท ์ „์†ก
โš ๏ธ **GitHub.com Fallback** โš ๏ธ