일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Bucket
- 샘플링
- nodejs
- multiparty
- 게임서버
- 지하철역
- Node
- 리사이즈
- 이미지서버
- 스트리밍서버
- Thumbnail
- 노선
- resize
- 시작하기
- 아날로그영상
- Sharp
- ProudNet
- 이미지
- 디지털영상
- streaming
- 좌표
- AWS
- 튜토리얼
- 이미지프로세싱
- lightsail
- 프라우드넷
- S3
- 데이터
- 버킷
- 화소
- Today
- Total
Deep Studying
[프라우드넷] 원카드 서버 만들기(1) 프로젝트 시작하기 본문
프로젝트 생성
이미 한 번 설명 드렸기 때문에 설명하는 과정은 생략하겠습니다.
프라우드넷을 처음 사용하신다면 아래 글을 따라 프로젝트를 시작해주세요. 이 때, ChattingServer, ChattingCommon, ChattingClient라고 되어있는 부분들의 Chatting이란 문구를 Onecard로 바꿔주세요
1. 프로젝트 생성
- OnecardServer ( C# 콘솔 앱 )
- OnecardCommon ( C# 클래스 라이브러리 )
- OnecardClient ( C# 콘솔 앱 )
2. PIDL 설정
- Common 프로젝트에 PIDL 폴더 생성
- Server, Client 프로젝트에 RMI 폴더 생성
- C2S.PIDL, S2C.PIDL 파일 생성
- PIDL.bat 파일 생성
3. 프로젝트 종속성 설정
- Common 프로젝트에 ProudDotNetClient.dll, ProudDotNetServer.dll 참조 추가
- Server, Client 프로젝트에 Common프로젝트, ProudDotNetClient.dll, ProudDotNetServer.dll 참조 추가
- Server, Client 디버그 폴더에 6개 파일 복사
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 클래스 구성하기
'게임서버' 카테고리의 다른 글
[프라우드넷] 원카드 서버 만들기(2) GameRoom 클래스 구성하기 (0) | 2022.01.30 |
---|---|
[프라우드넷] 채팅 서버 만들기(4) Dictionary 사용해보기 / Room으로 관리하기 (0) | 2022.01.13 |
[프라우드넷] 채팅 서버 만들기 (3) Marshaler 이용하기 (2) | 2022.01.12 |
[프라우드넷] 채팅 서버 만들기(2) RMI 통신하기 (0) | 2022.01.11 |
[프라우드넷] 채팅 서버 만들기(1) 프로젝트 초기 설정하기 (0) | 2022.01.10 |