Deep Studying

[프라우드넷] 원카드 서버 만들기(1) 프로젝트 시작하기 본문

게임서버

[프라우드넷] 원카드 서버 만들기(1) 프로젝트 시작하기

miniSeop 2022. 1. 19. 12:20

 

 

 

 

프로젝트 생성

이미 한 번 설명 드렸기 때문에 설명하는 과정은 생략하겠습니다.

프라우드넷을 처음 사용하신다면 아래 글을 따라 프로젝트를 시작해주세요. 이 때, ChattingServer, ChattingCommon, ChattingClient라고 되어있는 부분들의 Chatting이란 문구를 Onecard로 바꿔주세요

 

채팅 서버 만들기(1) 프로젝트 초기 설정하기

 

 

1. 프로젝트 생성

- OnecardServer ( C# 콘솔 앱 )

- OnecardCommon ( C# 클래스 라이브러리 )

- OnecardClient ( C# 콘솔 앱 )

2. PIDL 설정

- Common 프로젝트에 PIDL 폴더 생성

- Server, Client 프로젝트에 RMI 폴더 생성

- C2S.PIDL, S2C.PIDL 파일 생성

더보기
더보기
더보기
[marshaler(cs) = OnecardCommon.Marshaler]
global C2S 2000
{
	Login([in] String UserName);
	EnterRoom([in] int RoomNumber);
	LeaveRoom();
}
[marshaler(cs) = OnecardCommon.Marshaler]
global S2C 3000
{
	ResponseLogin([in] OnecardCommon.User user);
}

 

- PIDL.bat 파일 생성

더보기
더보기
더보기
cd %~dp0
"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" -cs .\PIDL\S2C.PIDL -outdir .\PIDL
"C:\Program Files (x86)\Nettention\ProudNet\util\PIDL.exe" -cs .\PIDL\C2S.PIDL -outdir .\PIDL
copy .\PIDL\*.cs ..\OnecardServer\RMI\
copy .\PIDL\*.cs ..\OnecardClient\RMI\
pause

 

3. 프로젝트 종속성 설정

- Common 프로젝트에 ProudDotNetClient.dll, ProudDotNetServer.dll  참조 추가

- Server, Client 프로젝트에 Common프로젝트ProudDotNetClient.dllProudDotNetServer.dll  참조 추가

- Server, Client 디버그 폴더에 6개 파일 복사

더보기
더보기
더보기

        libcrypto-1_1-x64.dll

        libssl-1_1-x64.dll

        ProudNetClient.dll

        ProudNetClientPlugin.dll

        ProudNetServer.dll

        ProudNetServerPlugin.dll

4. 기본 코드 작성

- Common > Common.cs

더보기
더보기
더보기
namespace OnecardCommon
{
	public class Vars
	{
		public static System.Guid m_Version = new System.Guid("{ 0x3ae33249, 0xecc6, 0x4980, { 0xbc, 0x5d, 0x7b, 0xa, 0x99, 0x9c, 0x7, 0x39 } }");
		public static int m_serverPort = 33334;

		static Vars()
		{

		}
	}
    
	public class User
	{
		static int UserId = 0;
		public HostID HostId { get; set; }
		public string UserName { get; set; }
		public int UserID { get; set; }
		public int RoomNumber { get; set; }
        
		public User(string UserName, HostID HostId)
		{
			UserID = ++UserId;
			this.UserName = UserName;
			this.HostId = HostId;
			RoomNumber = 0;
		}
		public User()
		{
			UserName = "Unknown";
			RoomNumber = 0;
		}
	}
}

- Common > Marshaler.cs

더보기
더보기
더보기
using Nettention.Proud;

namespace OnecardCommon
{
    public class Marshaler : Nettention.Proud.Marshaler
    {
        public static void Write(Message msg, User user)
        {
            msg.Write(user.HostId);
            msg.Write(user.UserName);
            msg.Write(user.UserID);
        }
        public static User Read(Message msg, out User user)
        {
            msg.Read(out HostID HostId);
            msg.Read(out string UserName);
            msg.Read(out int UserID);

            user = new User();
            user.HostId = HostId;
            user.UserName = UserName;
            user.UserID = UserID;

            return user;
        }
    }
}

 

- Server > Program.cs

더보기
더보기
더보기
namespace OnecardServer
{

    class Program
    {
        static void Main()
        {
            ServerLauncher server = new ServerLauncher();

            try
            {
                server.ServerStart();
                Console.Write("Server started\n");

                while (server.RunLoop)
                {
                    if (Console.KeyAvailable && Console.ReadKey(true).Key == ConsoleKey.Escape && Console.ReadKey(true).Key == ConsoleKey.Delete)
                    {
                        break;
                    }
                    System.Threading.Thread.Sleep(1000);
                }

                Console.Write("Server Closed\n");
                server.Dispose();

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

- Server > ServerLauncher.cs

더보기
더보기
더보기
using OnecardCommon;
using Nettention.Proud;

namespace OnecardServer
{
    public class ServerLauncher
    {
        public bool RunLoop;
        public static readonly NetServer NetServer = new NetServer();
        private readonly Nettention.Proud.ThreadPool _netWorkerThreadPool = new Nettention.Proud.ThreadPool(8);
        private readonly Nettention.Proud.ThreadPool _userWorkerThreadPool = new Nettention.Proud.ThreadPool(8);

        Handler Handler = new Handler();
        process.CommonProcess Process = new process.CommonProcess();

        public static ConcurrentDictionary<HostID, User> UserList { get; } = new ConcurrentDictionary<HostID, User>();
        
        public void InitializeStub()
        {
            Process.InitStub();
        }
        public void InitializeHandler()
        {
            NetServer.ConnectionRequestHandler = Handler.ConnectionRequestHandler;
            NetServer.ClientHackSuspectedHandler = Handler.ClientHackSuspectedHandler;
            NetServer.ClientJoinHandler = Handler.ClientJoinHandler;
            NetServer.ClientLeaveHandler = Handler.ClientLeaveHandler;
            NetServer.ErrorHandler = Handler.ErrorHandler;
            NetServer.WarningHandler = Handler.WarningHandler;
            NetServer.ExceptionHandler = Handler.ExceptionHandler;
            NetServer.InformationHandler = Handler.InformationHandler;
            NetServer.NoRmiProcessedHandler = Handler.NoRmiProcessedHandler;
            NetServer.P2PGroupJoinMemberAckCompleteHandler = Handler.P2PGroupJoinMemberAckCompleteHandler;
            NetServer.TickHandler = Handler.TickHandler;
            NetServer.UserWorkerThreadBeginHandler = Handler.UserWorkerThreadBeginHandler;
            NetServer.UserWorkerThreadEndHandler = Handler.UserWorkerThreadEndHandler;
        }
        public void InitialzieServerParameter()
        {
            var parameter = new StartServerParameter();
            parameter.protocolVersion = new Nettention.Proud.Guid(Vars.m_Version);
            parameter.tcpPorts.Add(Vars.m_serverPort);
            NetServer.Start(parameter);
        }
        public void ServerStart()
        {
            InitializeHandler();
            InitializeStub();
            InitialzieServerParameter();
            RunLoop = true;
        }
        public void Dispose()
        {
            NetServer.Dispose();
        }
    }
}

Server > Handler.cs

더보기
더보기
더보기
using System.Diagnostics.CodeAnalysis;
using Nettention.Proud;
namespace OnecardServer
{
    internal class Handler
    {
        process.CommonProcess Process = new process.CommonProcess();

        public bool ConnectionRequestHandler(AddrPort clientAddr, ByteArray userDataFromClient, [NotNull] ByteArray reply)
        {
            reply = new ByteArray();
            reply.Clear();

            return true;
        }

        public void ClientHackSuspectedHandler(HostID clientId, HackType hackType)
        {

        }

        public void ClientJoinHandler(NetClientInfo clientInfo)
        {

        }

        public void ClientLeaveHandler(NetClientInfo clientInfo, ErrorInfo errorinfo, ByteArray comment)
        {

        }

        public void ErrorHandler(ErrorInfo errorInfo)
        {

        }

        public void WarningHandler(ErrorInfo errorInfo)
        {

        }

        public void ExceptionHandler(Exception e)
        {

        }

        public void InformationHandler(ErrorInfo errorInfo)
        {

        }

        public void NoRmiProcessedHandler(RmiID rmiId)
        {

        }

        public void P2PGroupJoinMemberAckCompleteHandler(HostID groupHostId, HostID memberHostId, ErrorType result)
        {

        }

        public void TickHandler(object contextBoundObject)
        {

        }

        public void UserWorkerThreadBeginHandler()
        {

        }

        public void UserWorkerThreadEndHandler()
        {

        }
    }
}

Server > process > Process.cs

더보기
더보기
더보기
using Nettention.Proud;
using OnecardCommon;

namespace OnecardServer.process
{
    public class CommonProcess
    {
        static S2C.Proxy S2CProxy = new S2C.Proxy();
        static C2S.Stub C2SStub = new C2S.Stub();

        public void InitStub()
        {
            C2SStub.Login = Login;
            C2SStub.EnterRoom = EnterRoom;
            C2SStub.LeaveRoom = LeaveRoom;

            ServerLauncher.NetServer.AttachProxy(S2CProxy);
            ServerLauncher.NetServer.AttachStub(C2SStub);
        }
        static public bool Login(HostID remote, RmiContext rmiContext, string UserName)
        {
            string message = string.Format("{0} entered.", UserName);
            User user = new User(UserName, remote);

            ServerLauncher.UserList.TryAdd(remote, user);

            S2CProxy.ResponseLogin(user.HostId, rmiContext, user);

            Console.WriteLine(message);
            return true;
        }

        // * 추가 * //
        static public bool EnterRoom(HostID remote, RmiContext rmiContext, int RoomNumber)
        {
            return true;
        }
        static public bool LeaveRoom(HostID remote, RmiContext rmiContext)
        {
            return true;
        }

    }
}

 

 

 

 

프로젝트 기획

Outgame

1. 유저는 로그인 할 수 있다. ( 채팅서버 때 처럼 ID, 비밀번호 등의 인증 없이 닉네임만 입력받아 입장합니다. )

2. 방은 0번부터 9번까지 미리 생성되어있다.

3. 한 방에는 4명까지만 입장 가능하고, 2명 이상일 때 게임을 시작할 수 있다.

4. 방은 게임중, 대기중 두 가지 상태를 가진다. 게임중인 방에는 들어갈 수 없고 방에서 나올 수도 없다.

 

Ingame

게임을 플레이하는 사람의 기준으로 중요한 객체, 할 수 있는 액션 두 가지로 나누어 생각했습니다.

원카드를 실제 카드로 플레이한다고 생각하면 눈에 띄는 요소는 다음과 같았습니다.

  - 내 손에 든 카드

  - 상대방 카드의 개수

  - 마지막으로 낸 카드

  - 누구의 턴인가?

 

그리고 내가 할 수 있는 액션은

  - 게임을 시작한다.

  - 카드를 낸다.

  - 카드를 드로우한다.

  - 카드의 모양을 변경한다. ( 7을 냈을 때 )

 

이렇게 네 가지였습니다. 이를 이용하여 PIDL을 먼저 구성하겠습니다.

 

 

PIDL 구성

Common > Common.cs

먼저 게임에 사용할 카드는 클라이언트와 서버 모두가 똑같이 사용할 것 같습니다. 따라서 이를 먼저 작성해줍니다.

User 클래스 아래에 추가해줍니다.

public class User
{

	...

}

public class GameCard
{
	public int shape;
	public int number;
	public GameCard()
	{

	}
	public GameCard(int number)
	{
		this.shape = number % 4 + 1;
		this.number = number / 4 + 1;
	}
	public int toNumber()
	{
		return (shape - 1) + (number - 1) * 4;
	}
	public string toString()
	{
		string shape = "";
		string number = "";
		switch (this.shape)
		{
			case 1:
				shape = "♠";
				break;
			case 2:
				shape = "♥";
				break;
			case 3:
				shape = "♣";
				break;
			case 4:
				shape = "◆";
				break;
			default:
				shape = "■";
				break;
		}
		switch (this.number)
		{
			case 0:
				number = " ";
				break;
			case 1:
				number = "A";
				break;
			case 10:
				number = "T";
				break;
			case 11:
				number = "J";
				break;
			case 12:
				number = "Q";
				break;
			case 13:
				number = "K";
				break;
			default:
				number = this.number.ToString();
				break;
		}
		return shape+number;
	}
	public bool Match(GameCard card)
	{
		return this.shape == card.shape || this.number == card.number;
	}
}

 

카드를 초기화하는 것은 단순하게 생각했습니다.

0~51의 숫자를 num을 넣고, num/4는 카드의 숫자로, num%4는 카드의 모양으로 구성하였습니다.

toNumber( ) 메서드는 뒤에 마샬링 과정에 사용하기 위해 미리 추가해두었습니다.

 

 

Common > Marshaler.cs

public class Marshaler : Nettention.Proud.Marshaler
{
    public static void Write(Message msg, GameCard card)
    {
        int num = card.toNumber();
        msg.Write(num);
    }
    public static GameCard Read(Message msg, out GameCard card)
    {
        msg.Read(out int num);
        card = new GameCard(num);

        return card;
    }
    
    ...
    
}

 

Marshaler에도  GameCard의 내용을 추가해줍니다.

 

 

C2S.PIDL ( Client -> Server )

[marshaler(cs) = OnecardCommon.Marshaler]
global C2S 2000
{
	Login([in] String UserName);
	EnterRoom([in] int RoomNumber);
	LeaveRoom();

	Start();
	PlayCard([in] OnecardCommon.GameCard card);
	DrawCard();
	ChangeShape([in] int shape);
}

 

채팅 서버에서 만들었던 Login, EnterRoom, LeaveRoom 외에도 네 가지 RPC 패킷이 추가되었습니다.

  - 게임을 시작한다.

  - 카드를 낸다.

  - 카드를 드로우한다.

  - 카드의 모양을 변경한다. ( 7을 냈을 때 )

이 요청들은 각각 위에서 언급했던 "할 수 있는 액션" 네 가지입니다.

 

 

S2C.PIDL ( Server -> Client )

[marshaler(cs) = OnecardCommon.Marshaler]
global S2C 3000
{
	ResponseLogin([in] OnecardCommon.User user);
	ResponseEnter([in] int RoomNumber, [in] int playerID);
    
	ResponseDraw([in] List<OnecardCommon.GameCard> cards);
	ChangeHand([in] int playerID, [in] int leftHand);
	ChangeLastCard([in] OnecardCommon.GameCard card);
	ChangeTurn([in] int playerID);

	NotifyStartGame([in] int firstPlayerID, [in] OnecardCommon.GameCard firstCard);
	NotifyEndGame([in] int winnerID);
}

인게임 요소와는 별개로 방에 입장 불가능한 경우가 추가되었으니 ResponseEnter( )라는 패킷을 추가했습니다.

 

위에서 언급했던 "중요한 객체" 네 가지가 있었습니다. 

 

  - 내 손에 든 카드      -> 카드 드로우시 ResponseDraw( )로 변경 / 카드를 냈을 때는 복합적으로 결정

  - 상대방 카드의 개수 ->  ChangeHand( )로 변경

  - 마지막으로 낸 카드 ->  ChangeLastCard( )로 변경

  - 누구의 턴인가?       ->  ChangeTurn( )으로 변경

 

서버에서는 이를 적절히 바꿔주는 요청을 하도록 합니다.

 

그리고 게임의 시작과 게임의 끝을 알리는 요청도 필요하니 추가해주었습니다.

이들은 각각 NotifyStartGame( )과 NotifyEndGame( ) 입니다.

 

 

Client > Program.cs

우선 클라이언트에 Stub을 모두 등록해주겠습니다.

 

using Nettention.Proud;
using OnecardCommon;

namespace Client
{
    class Program
    {
        static object g_critSec = new object();
        static NetClient netClient = new NetClient();
        static S2C.Stub S2CStub = new S2C.Stub();
        static C2S.Proxy C2SProxy = new C2S.Proxy();
        static bool isConnected = false;
        static bool isLoggedin = false;
        static bool isPlaying = false;
        static bool keepWorkerThread = true;

        static User me = new User();

        static void InitializeStub()
        {
            S2CStub.ResponseLogin = (HostID remote, RmiContext rmiContext, User user) =>
            {
                lock (g_critSec)
                {
                    isLoggedin = true;
                    me = user;
                }
                return true;
            };

            S2CStub.ResponseEnter = (HostID remote, RmiContext rmiContext, int RoomNumber, int playerID) =>
            {
                return true;
            };
            S2CStub.ResponseDraw = (HostID remote, RmiContext rmiContext, List<GameCard> hand) =>
            {
                return true;
            };

            S2CStub.ChangeHand = (HostID remote, RmiContext rmiContext,int playerID, int count) =>
            {
                return true;
            };
            S2CStub.ChangeLastCard = (HostID remote, RmiContext rmiContext, GameCard card) =>
            {
                return true;
            };
            S2CStub.ChangeTurn = (HostID remote, RmiContext rmiContext, int playerID) =>
            {
                return true;
            };

            S2CStub.NotifyStartGame = (HostID remote, RmiContext rmiContext, int firstPlayer, GameCard firstCard) =>
            {
                return true;
            };
            S2CStub.NotifyEndGame = (HostID remote, RmiContext rmiContext, int winnerID) =>
            {
                return true;
            };
        }
        static void InitializeHandler()
        {
            netClient.JoinServerCompleteHandler = (info, replyFromServer) =>
            {
                lock (g_critSec)
                {
                    if (info.errorType == ErrorType.Ok)
                    {
                        Console.Write("Succeed to connect server. Allocated hostID={0}\n", netClient.GetLocalHostID());
                        isConnected = true;
                    }
                    else
                    {
                        Console.Write("Failed to connect server.\n");
                        Console.WriteLine("errorType = {0}, detailType = {1}, comment = {2}", info.errorType, info.detailType, info.comment);
                    }
                }
            };

            netClient.LeaveServerHandler = (errorInfo) =>
            {
                lock (g_critSec)
                {
                    Console.Write("OnLeaveServer: {0}\n", errorInfo.comment);

                    isConnected = false;
                    keepWorkerThread = false;
                }
            };

        }
        static void initializeClient()
        {
            netClient.AttachStub(S2CStub);
            netClient.AttachProxy(C2SProxy);
        }
        static void InitializeClientParameter()
        {
            NetConnectionParam cp = new NetConnectionParam();
            cp.protocolVersion.Set(Vars.m_Version);
            cp.serverIP = "localhost";
            cp.serverPort = (ushort)Vars.m_serverPort;
            netClient.Connect(cp);
        }
        static void Draw()
        {
            Console.Clear();
        }

        static void Main(string[] args)
        {
            InitializeHandler();
            initializeClient();
            InitializeStub();
            InitializeClientParameter();

            Thread workerThread = new Thread(() =>
            {
                while (keepWorkerThread)
                {
                    Thread.Sleep(10);
                    netClient.FrameMove();
                }
            });
            workerThread.Start();

            // Connection
            while (!isConnected)
                Thread.Sleep(1000);

            // Login
            while (!isLoggedin)
            {
                Console.Write("UserName: ");
                string userInput = Console.ReadLine();
                if (userInput == "")
                    continue;

                Console.WriteLine("Login...");
                C2SProxy.Login(HostID.HostID_Server, RmiContext.ReliableSend, userInput);
                Thread.Sleep(1000);
            }

            // Enter to game room
            while(me.RoomNumber == 0)
            {
                string userInput = Console.ReadLine();
                if (userInput == "")
                    continue;
                try
                {
                    int RoomNumber = Int32.Parse(userInput);
                    C2SProxy.EnterRoom(HostID.HostID_Server, RmiContext.ReliableSend, RoomNumber);
                }
                catch (FormatException ex)
                {

                }
                Thread.Sleep(1000);
            }

            // Playing
            while (keepWorkerThread)
            {
                if (Console.KeyAvailable)
                {
                    
                }
                System.Threading.Thread.Sleep(30);
            }

            workerThread.Join();
            netClient.Disconnect();
        }
    }
}

 

초점을 서버에만 맞추다보니 클라이언트의 메인 루프가 매우 길고 비효율적으로 작성될 것 같습니다.

Unity로 클라이언트도 같이 만들어보실 분들은 Proxy/Stub의 구성만 간단히 보시길 권해드립니다.

 

 

Server > process / GameRoomProcess.cs

process 디렉토리에 GameRoomProcess.cs 클래스 파일을 생성해줍니다.

 

이 파일 안에는 게임 룸 안에서 실행되는 프로시저들을 작성하겠습니다.

 

using Nettention.Proud;
using OnecardCommon;

namespace OnecardServer.process
{
    internal class GameRoomProcess : CommonProcess
    {

        static public bool Start(HostID remote, RmiContext rmiContext, int firstPlayerID, GameCard firstCard)
        {
            return true;
        }
        static public bool PlayCard(HostID remote, RmiContext rmiContext, GameCard card)
        {
            return true;
        }
        static public bool DrawCard(HostID remote, RmiContext rmiContext)
        {
            return true;
        }
        static public bool ChangeShape(HostID remote, RmiContext rmiContext, int shape)
        {
            return true;
        }
    }
}

 

Server > GameRoom.cs

서버에 GameRoom 클래스를 하나 생성해줍니다. 내용은 다음 포스트에서 채우도록 하겠습니다.

 

여기까지 진행하셨다면 프로젝트를 시작하기 위한 사전 준비를 마치셨습니다.

그리고 서버의 파일트리는 다음과 같을 것입니다.

 

 

새로운 내용에 비해 여러 가지를 다시 한 번 설명하느라 분량이 길어진 것 같네요

 

다음 포스트에서는 본격적으로 GameRoom클래스의 내용을 작성하며 프로젝트를 시작하도록 하겠습니다.

 

다음 포스트: 원카드 서버 만들기(2) GameRoom 클래스 구성하기