직접 서버를 돌리지 않는, Cirrus와 같은 서비스를 찾다가 발견한 서비스.
야후가 대신 서버를 돌려주는 방식이다.
링크 : https://gamesnet.yahoo.net/documentation/services/multiplayer/
현재 무료로 제공된다.
(듣기로는 Big DB라는 게임 상의 데이터를 저장하는 데이터베이스의 역할을 하는 서비스도 무료로 제공된다고 한다.)
한글 자료를 찾아보고자 노력했지만, 찾은 것은 한 블로그 뿐이다. 직접 구현까지 한 뒤,
소스까지 상세히 올려놓으셨으니, 영어는 도저히 안 되겠다는 사람은 다음 블로그를 참고하면 되겠다.
(하지만 야후 사이트에서도 매우 친절하게 설명하고 있어 웬만하면 야후에서 제공하는 설명문을 읽어보는 것을 추천한다.)
위의 블로그에서 언급하는 'YGN'의 특징은 다음과 같다.
1. 클러스터링 할 수 있다. 즉 서버확장이 가능하다.
2. 유용한 룸 기능(채팅방처럼 유저간 그룹을 묶을 수 있죠)
3. 서버코드는 닷넷(C#, VB.NET)으로 개발하면 된다.
4. 클라이언트는 플래시,유니티3D,닷넷,안드로이드,아이폰으로 개발 할 수 있다.
5. 보안 우수하다.
6. 로컬PC에 설치해서 쓰는 개발&디버깅용 서버도 준다.
테스트해보고 싶겠지만, 그 전에 위의 블로그만으로는 부족한 '설명'을 보충하기 위해 원문을 참고하자.
다음은 YGN에서 공식 제공하는 '설명서' 중 Server 와 Client 파트를 따와 내 나름대로의 해석을 적어본 것이다.
정말 간단하게 설명하고 있는 정보에 대한 것을 살짝 적어놓은 것이니, 전혀 감도 안 잡힐 때 읽으면 도움이 될 것이다.
Rooms and Players - 방, 플레이어
Serverside code for an entire game is divided into one or more Room Types, and one or more Player classes. When a player creates a room of a specific type, an instance of that Room Type is created on a game server and starts running.
Whenever a player then joins that room, his client will connect to that game server, and an instance of the corresponding Player object is created. Consequently, when a player leaves a room his Player object is destroyed, and when everyone has left a room, that Room Type object is destroyed. This means that no serverside code can run if there are no players connected, and that you have to take care to persist any player or game state beforehand.
The distributed nature of the system means that players in the same room will always be connected to the same game server, but different rooms of the same room type might run on different servers in different physical locations. This means that no data can be shared between different rooms of the same game.
간단하게 요약하자면 서버 측에서는 '방'과 '플레이어'의 클래스를 다루며, 같은 방에서는 클라이언트간 데이터 공유가 가능하고 다른 방에서는 클라이언트간 데이터의 공유가 불가하다는 내용이다.
Base Classes - 기본 클래스
In a nutshell, the serverside code is largely event-based and should be built to respond to various events such as players connecting, disconnecting, and sending in messages to the server.
Here's a bare-bones example of serverside code that allows all players to join, and every time a player sends a message to the server, it will be broadcast to all connected players:
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 32 33 34 35 36 37 38 39 | public class MyPlayer : BasePlayer { } [RoomType( "MyRoomType" )] public class MyGame : Game<MyPlayer> { //This method is called when a room is created public override void GameStarted() { //Perform actions to initialize room here } //This method is called when the last player leaves the room. public override void GameClosed() { //Do any clean-up here such as saving statistics } // This method is called whenever a player joins the room public override void UserJoined(MyPlayer player) { //Notify other players that someone joined, //or inform the new player of the game state } // This method is called when a player leaves the room public override void UserLeft(MyPlayer player) { //Notify other players the someone left } //This method is called before a user joins a room. //If you return false, the user is not allowed to join. public override bool AllowUserJoin(MyPlayer player) { return true ; } //This method is called whenever a player sends a message to the room public override void GotMessage(MyPlayer player, Message message) { //Handle all different message types here //This room broadcasts all incoming messages //to all connected players: Broadcast(message); } } |
Inheriting from BasePlayer
The first thing you must do in your serverside code is to make a player class that inherits from BasePlayer.
Since you are inheriting from another class, there will already be some existing properties and methods, but you are otherwise free to add whatever you need that is specific to your game. For each player that connects to your game, an instance of this class will be created, and each game room will contain a list of these.
What you typically want to add to your player class is the state of each player, for example their position in the world, their inventory, their hand of cards, their weapons, their score, etc.
For example, the player class of Fridge Magnets looks like this:
1 2 3 4 5 6 7 8 | public class Player : BasePlayer { public int X; public int Y; public Player() { X = 0; //Player mouseX Y = 0; //Player mouseY } } |
Inheriting from Game<P>
The second thing you must do in your serverside code is to make a game class that inherits from Game<P>.
Just like the base player class, the base game class also has some existing properties and methods, and you need to override some methods to be able to respond to the various events.
You are also free to add any other properties and methods that you need for your game, and what you typically add to your game class is everything that is shared between the players in a room. If the player class holds the state of each player, the game class holds the state of their world. For example, this is where you would store the position of all monsters in the world, the deck of cards in a card game, whos turn it is to go next, a log of chat messages, etc.
To continue the above example, this is how the game class for Fridge Magnets begins. It defines a roomtype, references the player class, and defines a collection of Letters for the game:
기본 클래스에 관한 내용이므로 읽어보는 것을 권장한다.
1 2 3 4 5 6 7 | [RoomType( "FridgeMagnets" )] public class GameCode : Game<Player> { //Create array to store our letters private Letter[] letters = new Letter[230]; //... } |
Game Started and Game Closed - 게임 시작/종료
The first two events that you need to handle is when a game starts and when a game ends. This happens when the first player connects to a room, and when the last player leaves a room.
You can handle these events by overriding the GameStarted and GameClosed methods.
Typically, you want to do all the housekeeping tasks in these methods such as creating a fresh new game world or saving the scores of a completed game.
In Fridge Magnets, all the letters are created in the GameStarted method, but we don't need to do anything when a game closes:
1 2 3 4 5 6 7 8 9 10 11 12 | public override void GameStarted() { Console.WriteLine( "Game is started" ); // Create 230 letters for ( int a = 0; a < letters.Length; a++) { letters[a] = new Letter(-1, -1); } } public override void GameClosed() { Console.WriteLine( "RoomId: " + RoomId); } |
User Joined and User Left - 플레이어 참여, 나가기
The next events are when a player joins the room and when a player leaves the room.
You can handle these events by overriding the UserJoined and UserLeft methods.
These events should be used first to inform a new player of the state of the world so they are synchronized with everyone else, and to inform the existing players whenever someone joins or leaves.
In Fridge Magnets, each player that joins gets the positions of all existing letters, and whenever someone leaves, all other players are informed of this:
플레이어가 서버(방)에 참여하고 나가는 것에 관여하는 UserJoined와 UserLeft 메서드에 관한 설명이다.
밑의 예제는 플레이어가 들어오면, 모두에게 Send 하는 Broadcast 명령을 이용하여 모두에게 새로운 플레이어의 참여를 알리는 내용이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | public override void UserJoined(Player player) { // Create init message for the joining player Message m = Message.Create( "init" ); // Tell player their own id m.Add(player.Id); //Add the current position of all letters to the init message for ( int a = 0; a < letters.Length; a++) { Letter l = letters[a]; m.Add(l.X, l.Y); } // Send init message to player player.Send(m); } public override void UserLeft(Player player) { Console.WriteLine( "Player " + player.Id + " left the room" ); //Inform all other players that user left. Broadcast( "left" , player.Id); } |
Allow User Join - 플레이어 접속 제한
By overriding the AllowUserJoin method you can control whether or not a player can join a room. If this method returns false, the player is not allowed to join, and consequently, the UserJoined method isn't called for this player either.
You only need to override this method if you need to control who can join a room or not, for example if you want to limit the amount of players, or if a player doesn't meet some other requirement for joining.
Here's an example that checks a value from roomdata to see if the current player should be allowed in or not:
AllowUserJoin 메서드를 이용하여 플레이어의 참가를 제한할 수 있다. 이 메서드는 '방' 개념을 가진 온라인 게임에서 필수적이라고 생각된다. 한 방에 참여할 수 있는 인원이 제한되어 있기 때문이다. 예제에서도 Roomdata 중 하나인 Max Players 를 확인하여 방이 꽉 차있으면 참여를 제한하는 방식을 설명하고 있다.
1 2 3 4 5 6 7 8 9 10 11 12 | public override bool AllowUserJoin(MyPlayer player) { int maxplayers; //Default //Parse roomdata if (! int .TryParse(RoomData[ "maxplayers" ], out maxplayers)) { maxplayers = 4; //Default } //Check if there's room for this player. if (Players.Count < maxplayers - 1) { return true ; } return false ; } |
Got Message - 메세지 수신
The most important event in your serverside code happens whenever a client sends a message to the server. This is handled by overriding the GotMessage method.
You always need to override this method, and you should add code that handles all the different message types that your client sends in. This is where the bulk of your game logic will be, this is where you react to actions by each client, and where you will send the results of those actions back out to the affected players.
If you look at the Fridge Magnets example below, there are only three message types that the client sends in: When a player moves a letter, when a player moves his mouse, and when a player activates a letter.
서버 측에서는 절대적으로 요구되는 메세지 수신 메서드이다. 정말 간단하게 메세지를 수신할 수 있다. 한 번 읽어보면 바로 이해될 것이라 예상된다.
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 | public override void GotMessage(Player player, Message message) { //Switch on message type switch (message.Type) { case "move" : { //Move letter in internal representation Letter l = letters[message.GetInteger(0)]; l.X = message.GetInteger(1); l.Y = message.GetInteger(2); //inform all players that the letter has been moved Broadcast( "move" , message.GetInteger(0), l.X, l.Y); break ; } case "mouse" : { //Set player mouse information player.X = message.GetInteger(0); player.Y = message.GetInteger(1); break ; } case "activate" : { Broadcast( "activate" , player.Id, message.GetInteger(0)); break ; } } } |
Broadcast and Send - Broadcast (모두에게 전송), Send (한 클라이언트에게 전송)
Very often you will want to send a message back to players as a reaction to a game event. By calling theBroadcast method, you can send a message to all players that are connected to the room.
If instead you only want to send a message to a specific player, you have to use the Send method which is on your player object. This means that you first have to get the correct player object, and then call the Send method.
The below example shows how to make the serverside code for a simple chat that allows public and private messages:
수신과 더불어 가장 기본적인 파트인 '발신' 파트에 대해 다루고 있는 부분이다. Send는 한 클라이언트에게 특정 데이터를 보내는 메서드이며, Broadcast는 이와 같은 동작을 모든 클라이언트에게 수행하는 메서드이다. 본 예제에서는 Broadcast 와 함께 '귓속말' 기능을 소개하고 있다. (받은 메세지를 분석한 뒤, 클라이언트가 요구하는 사람을 접속자 중에서 찾아, 그 사람에게만 Send 한다.)
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | public class Chatter : BasePlayer {} [RoomType( "Chat" )] public class Chat : Game<Chatter> { public override void UserJoined(Chatter chatter) { //Send the id to the chatter that joined //so he will know his own identity chatter.Send( "id" , chatter.ConnectUserId) //Send the id of the chatter that joined //to everyone that is already in the room Broadcast( "joined" , chatter.ConnectUserId); } public override void UserLeft(Chatter chatter) { //Send the id of the chatter that left //to everyone that is still in the room Broadcast( "left" , chatter.ConnectUserId); } public override void GotMessage(Chatter chatter, Message message) { switch (message.Type) { case "public" : //Send back the public message to everyone, together with //the id of the sending chatter. Clients should then only //display public messages from other chatters. Broadcast( "public" , chatter.ConnectUserId, message.GetString(0)); break ; case "private" : //Get the id of the recipient string recipient = message.GetString(0); //Find the recipient in the Players collection foreach (Chatter chatter in Players) { if (chatter.ConnectUserId = recipient) { //Send the private message chatter.Send( "private" , message.GetString(1)); } } break ; } } } } |
Timers - 타이머
Sometimes you want your serverside code to take an action that isn't immediately triggered by an incoming event. To accomplish this you can set up different timers.
To set up a one-time timer, you can use the ScheduleCallback method, and to set up a recurring timer, you can use the AddTimer method.
You can use timers for everything periodic in your game such as broadcasting the state of the world, NPC behaviour, random events and much more.
In Fridge Magnets, we set up a timer when each room is created to broadcast the positions of other players every 100 milliseconds like this:
말 그대로 타이머 기능으로, 작업을 얼마 뒤에 시행하거나 할 때 등에 쓰일 수 있는 메서드이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public override void GameStarted() { // add a timer that sends out an update every 100th millisecond AddTimer( delegate { //Create update message Message update = Message.Create( "update" ); //Add mouse cordinates for each player to the message foreach (Player p in Players) { update.Add(p.Id, p.X, p.Y); } //Broadcast message to all players Broadcast(update); }, 100); } |
Backend Services - 그 외 Backend 서비스
Finally, you can access all of the Backend Services from your serverside code.
You can use BigDB for storing persistent data about your players or your game such as highscores, rankings, inventories, achievements etc.
1 2 3 4 | if (!player.PlayerObject.Contains( "name" )) { player.PlayerObject.Set( "name" , player.Name); player.PlayerObject.Save(); } |
PayVault is fully accessible from serverside code. Although you can allow player's vaults to be manipulated from clientside code, we recommend that you do all such things from serverside code since it is a secure environment.
1 2 3 4 | player.PayVault.Refresh(); if (player.PayVault.Has( "simplecar" )) { //... } |
Note that before you can use the PlayerObject or PayVault property of your Player class, these have to be loaded with the asynchronous methods GetPlayerObject and Refresh.
To make this process easier you can set up your room so that the player object and vault are automatically preloaded before each player joins, by setting the PreloadPlayerObjects and PreloadPayVaults properties on the room to true:
1 2 3 4 | public override void GameStarted() { this .PreloadPlayerObjects = true ; this .PreloadPayVaults = true ; } |
To make debugging of your game easier you can use the ErrorLog service.
1 2 3 | if (player.Name == null ) { PlayerIO.ErrorLog.WriteError( "Player with no name connected." ); } |
If you need to get any form of resources into your serverside code, you can either embed the resource in the game DLL that you upload and then use EmbeddedResource to retrieve it, or you can host the resource on an external server and use the Web class to retrieve it.
1 2 3 4 | public byte [] mapdata; public override void GameStarted() { mapdata = EmbeddedResource.GetBytes( "data.map" ); } |
Create Room - 방 만들기
Creating a room is pretty straight-forward, you just need to call the CreateRoom method with the appropriate arguments.
If you don't supply your own room id, a random id will be generated for you by the server.
제목 그대로, 방을 만드는 메서드이다. 방의 ID를 지정해주지 않으면, 서버에서 랜덤으로 생성한다고 한다.
1 2 3 4 5 6 7 8 | client.createRoom( null , //Get random id from server "MyRoomType" , //Type of room true , //Should be visible {maxplayers: 8 , name: "Awesome Game!" }, //Extra data function (roomId: String ): void { trace ( "Created room" ) }, function (e:PlayerIOError): void { trace ( "Unable to create room" , e) } ); |
Room Data
The room data can be supplied when you create a room from a client, and in your serverside code you can both read and modify this data through the RoomData property.
Typically you should use room data to allow players to customize a specific game, for example by selecting a specific map, or deciding how many max players there can be.
The room data is also available when you list rooms, so if you build some sort of lobby system, you can display the settings of already created rooms that are waiting for players to join.
Roomdata는 해당 방의 데이터로, 로비에서 방을 나열하거나 정렬할 때 유용하게 사용할 수 있다.
List Rooms - 방을 나열
After various clients have created rooms you can list those rooms and search among them with the ListRoomsmethod. You have to specify a roomtype, and you will only get back rooms that are currently visible.
It is recommended that you use the resultLimit and resultOffset arguments to implement a paging system so that you don't list all existing rooms all the time.
You can also use the searchCriteria argument to filter the room list based on the roomData properties of the rooms. However, to use this feature you must first define which properties are searchable under the Settings tab of the Multiplayer Control Panel for your game. Below is an example that lists only rooms where the maxplayers property is set to 4, and only lists the 20 first rooms:
이 부분은 아직 정확하지 않은 부분이 있지만, 기본적으로는 특정 조건에 맞는 방을 나열해주는 메서드인 것 같다. 예제에서는 최대 플레이어가 4명인 방을 검색하는 부분을 설명한다.
1 2 3 4 5 6 7 8 | client.multiplayer.listRooms( "MyRoomType" , //Type of room {maxplayers: 4 }, //Only list rooms where maxplayers = 4 20 , //Limit to 20 results 0 , //Start at the first room function (rooms: Array <RoomInfo>): void { trace ( "Found: " + rooms.Length) }, function (e:PlayerIOError): void { trace ( "Unable to list rooms" , e) } ); |
Join Room - 방에 참여
To make a player join a room, simply call the JoinRoom method. The only required argument to this method is the id of the room to join, and the given room of course has to exist for this method to succeed.
When a player is joining a room, the server will first perform a check by running the AllowUserJoin method. If you override this in your serverside code you can conditionally allow the user to join or not.
방에 참여하는 메서드이다. 방에 참여하기 전, 서버 측에서 언급되었던 AllowUserJoin 메서드에 의해 제한 여부가 결정된다고 한다.
1 2 3 4 5 6 | client.multiplayer.joinRoom( "lobby" , //Join room with id "lobby" {name: 'Henrik' }, //Send extra join data function (connection:Connection): void { trace ( "Joined lobby" ) }, function (e:PlayerIOError): void { trace ( "Unable to join room" , e) } ); |
Create and Join Room - 방 생성과 동시에 참여
Finally there's a convenience method that allows you to create and join a room at the same time that is simply called CreateJoinRoom. If the room already exists, the player will join the room directly, and if it doesn't exist, it will be created with.
This method takes the same arguments as CreateRoom and JoinRoom does separately, and you can omit the room id to get one generated for you just like in the regular method.
방을 생성함과 동시에 참여하는 메서드이다. 단, 이미 방이 존재하면 방을 생성하지 않고 해당 방에 바로 참여하게 된다.
Service Rooms
If you supply $service-room$
as the id the player will join a special service room.
Service rooms have random names, and as soon as a room is 75% full, a new room will be created and new players that join will go to that room instead, thus ensuring that a minimum of these rooms are created and that players are balanced evenly into them.
This type of room is very useful when it's important that your players have a connection to a room, but when it doesn't really matter which other players are in this room. That rooms won't fill up completely also means that you can move players around the rooms if you want two specific players to exchange messages. This feature makes it really easy to build a distributed lobby system or matchmaking system, for example.
'서비스룸'은 플레이어간 매칭을 할 때 유용하게 사용될 것이라 한다. 정확히 이해하지는 못했으므로 자세한 정보는 원문을 참고하길 바란다.
1 2 3 4 5 6 7 8 9 | client.multiplayer.createJoinRoom( "$service-room$" , //Join service room "MyRoomType" , //Type of room false , //Invisible null , //No room data null , //No join data function (connection:Connection): void { trace ( "Joined service room" ) }, function (e:PlayerIOError): void { trace ( "Unable to join room" , e) } ); |
서버를 계속해서 가동을 할 수 없는 상황에서, 매우 좋은 방안이 될 수 있을 것으로 예상된다.
빠른 시일 내에 테스트해보고 싶다.
+ 추가 ) 제작 과정을 상세히 적어놓은 블로그 :
https://jacklehamster.wordpress.com/2014/06/10/tutorial-create-realtime-multiplayer-games-using-player-io-part-1-server/ (물론 영어지만, 큰 도움이 될 것으로 보인다.)
'Programming' 카테고리의 다른 글
[AS3] 효율적인 배열 섞기 함수 (0) | 2015.07.27 |
---|---|
[AS3] 소켓 통신 시 데이터가 밀린다면? (0) | 2015.01.23 |