ํด๋น ํ๋ก์ ํธ๋ ๊ฒ์ ์๋ฒ ๊ฐ๋ฐ์ ์ฃผ์ ๋ชฉ์ ์ผ๋ก ํฉ๋๋ค.
ํด๋ผ์ด์ธํธ๋ ์๋ฒ์ ํต์ ํ๊ณ ๋์์ ๋ณด์ฌ์ฃผ๊ธฐ ์ํด Unity ์์ง์ ์ฌ์ฉํ์ต๋๋ค.
ํ๋ก์ ํธ๋ 2D Top down MMORPG ์ฅ๋ฅด์ ๊ฒ์์ผ๋ก '๋ฐ๋์ ๋๋ผ' ์ผ๋ถ๋ฅผ ๋ฒค์น๋งํน ํ์ต๋๋ค.
Windows IOCP ์๋ฒ๋ก ์ฐ๊ฒฐ์ TCP/IP, ์ง๋ ฌํ ๋๊ตฌ๋ Flatbuffers๋ฅผ ์ฌ์ฉํ์ต๋๋ค.
1. ๋ฉํฐ์ฐ๋ ๋ ํ๋ก๊ทธ๋๋ฐ
์์์ ์ฐ์ฐ์ ์ํด atomic๊ณผ compare_exchange_strong() ํจ์ ๋ฑ ํ์ต.
์ฐ๋ ๋ ๋ง๋ค ์์ ์ ์ ์ฅ์๋ก ์ฌ์ฉํ ์ ์๋ Thread Local Storage ํ์ต.
ThreadManager ํด๋์ค๋ก TLS ์ด๊ธฐํ ๋ฐ ์ฝ๋ฐฑ ํจ์๋ฅผ ๋ฐ์์ ์คํ ํ์ต.
์ฝ๊ธฐ์ ์ฐ๊ธฐ ์์
์ ๋ฐ๋ผ ์ ์ฉํ ์ ์๋๋ก Reader-Writer Lock์ SpinLock ๋ฐฉ์์ผ๋ก ๊ตฌํํ๊ณ RAII๋ฅผ ์ฌ์ฉํ Lock ํด๋์ค ๊ตฌํ ํ์ต.
ํน์ ๊ฐ์ฒด๊ฐ ์ฐธ์กฐ๋๊ณ ์์์๋, ์ญ์ ํ๋ฉด์ ๋ฐ์ํ๋ ๋ฌธ์ ๋ฅผ ๊ด๋ฆฌํ๊ธฐ ์ํ Reference Counting ๊ฐ๋
ํ์ต.
์ค๋งํธ ํฌ์ธํฐ ํ์ต.
๊ธฐ๋ณธ new, delete ์ฐ์ฐ์ ๋์ ์ ์ปค์คํฐ๋ง์ด์งํ Allocator๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ ํ์ต.
๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ญ์ ํ๊ณ ์ ๊ทผ๋ ๋ง์์ผ๋ก์จ ์ค์ผ๋ ๋ฉ๋ชจ๋ฆฌ์ ์ ๊ทผ์ ์ฐจ๋จํ๋ ๊ฒ์ผ๋ก, ๊ฐ๋ฐ ๋จ๊ณ์์ ์ ์ฉํ Stomp ๋ฐฉ์ Allocator์ ํ์ต.
STL์์๋ ๋ง์ฐฌ๊ฐ์ง๋ก new, delete ๋์ ์ ์ฌ์ฉํ๋๋ก STL Allocator ํ์ต.
๋ฉ๋ชจ๋ฆฌ ํ๊ณผ ์ค๋ธ์ ํธ ํ ํ์ต.
3. ๋คํธ์ํฌ ํ๋ก๊ทธ๋๋ฐ
์์ผ ํ๋ก๊ทธ๋๋ฐ ๊ธฐ๋ณธ ํ์ต.
TCP์ UDP์ ํน์ง ๋ฐ ์ฐ๊ฒฐ ํ์ต.
๋ค์ํ ์์ผ ์ต์
์ ๋ํ ํ์ต.
::ioctlsocket() ํจ์๋ฅผ ์ฌ์ฉํ ๋
ผ๋ธ๋กํน ์์ผ ํ์ต.
๋ค์ํ ์์ผ ๋ชจ๋ธ ํ์ต.(select, WSAEventSelect, Overlapped(์ด๋ฒคํธ/์ฝ๋ฐฑ), Completion Port)
2. ์ฝ๋ ์์ค ์ผ๋ถ ์๊ฐ
ํ์๊ฐ์
๋ฐ ๋ก๊ทธ์ธ
์บ๋ฆญํฐ ์์ฑ ๋ฐ ์บ๋ฆญํฐ ์ ํ
์ธ๋ฒคํ ๋ฆฌ ํ๋ฉด ๋ฐ ์บ๋ฆญํฐ ์ ๋ณด
์ฑํ
NPC์ ์ํธ์์ฉ
๋ค๋ฅธ ํ๋ ์ด์ด ์ ๋ณด
๊ฑฐ๋
ํ์ผ๋งต์ ์ฌ์ฉํด์ ๋งต ์ ์
์บ๋ฆญํฐ ์ด๋ ๋ฐ ์ ๋๋ฉ์ด์
๊ธฐ๋ฅ
NPC์๊ฒ ๋ฌผ๊ฑด์ ์ฌ๊ณ ํ๋ ๊ธฐ๋ฅ
๋ค๋ฅธ ํ๋ ์ด์ด ์์ฑ ๋ฐ ์ด๋ ์ ๋๋ฉ์ด์
๊ธฐ๋ฅ
๋ค๋ฅธ ํ๋ ์ด์ด ํด๋ฆญ ์ด๋ฒคํธ ๊ธฐ๋ฅ(์ ๋ณด ๋ณด๊ธฐ, ๊ฑฐ๋ ์์ฒญ ๋ฒํผ ๋ฑ)
์ธ๋ฒคํ ๋ฆฌ, ๊ฑฐ๋, NPC ์ํธ์์ฉ์ ์ฌ์ฉ๋๋ ์์ดํ
๋ฆฌ์คํธ ๋ฑ์ ์์ฑ ๋ฐ ์ ํํ๋ ๊ธฐ๋ฅ
์ ํ ์, UX๋ฅผ ์ํด ์ ํ ๋ ์ค๋ธ์ ํธ ์์ ๋ณ๊ฒฝ ๊ธฐ๋ฅ
2. ์์ค ์ฝ๋ ์ผ๋ถ ์๊ฐ
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๋ฅผ ๋ฒํผ์ ๋ฃ์
// ํจํท ํค๋ ๊ตฌ์กฐ์ฒด ์ ์
[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 ํด๋์์ ๊ด๋ฆฌ
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 ๋ณ๊ฒฝ ์์
๋ฑ ์์ฒญ
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);
}
ํ๋ ์ด์ด๊ฐ ํน์ ํ๋์ ํ๋ฉด ๊ทธ์ ๋ง๋ ํจํท์ ์์ฑํด์ ์๋ฒ๋ก ํจํท ์ ์ก