heyVR SDK offers a comprehensive set of tools for developing multiplayer games. These tools cover both Matchmaking and Multiplayer features.
We will call the combination of all of our matchmaking, multiplayer, game lobbies, lobby-moderation, etc the Arena.
The heyVR arena is composed of 4 main components:
The game server that heyVR uses offers various features such as lobby state, message broadcasting and so on. All of these features are available to use via the gameplay SDK.
In order to be able to use the arena, developers must define rooms for their game. You can think of this room as a map, it wouldn't make sense to provide multiplayer system for a game without defining a map and configuring it.
To create a room for a game, you can head over to My Games > "Game Name" > APIs > Matchmaking in the developer area and click "Create Room".
A room is basically a blueprint for the lobbies that will be created. They consist of 2 important things:
Each room will automatically be assigned a default team upon creation, and if your game does not require the concept of having multiple teams, you can simply forget it from here on. Otherwise, you can create, update and delete teams after the room has been created.
The total number of players that can join a lobby is the sum of the players of each team, up to a total of maximum 16 players.
Important Note: Changing the room signature or modifying the teams in any way will cause all of the players that are currently using this room to be disconnected and results in the lobby being destroyed. This feature should not be used regularly.
After a room is created, developers can now create lobbies on behalf of the user using the SDK. A lobby is essentially an instance of a room.
Imagine, if a room was a JavaScript class named
Room
, then a lobby would be an instance of that class made usingconst lobby = new Room();
Lobbies have a lifespan. They are created, and automatically destroyed when no longer needed. We will explain this later.
Since the multiplayer requires a game server engine to function, developers need to connect their local testing to the heyVR platform to test the arena functionalities. But don't worry, these steps are very simple.
To do this, you'll need to take 2 small steps:
As mentioned above, simply create a room in the developer area. Same steps are creating a room for public use.
Before you connect to the game server, you will need to set 2 configuration options, the API token and the game slug.
The arena requires a valid authentication token to function. Logged-in users on heyVR platform have a cookie set that is responsible for authenticating with the game server. Since you don't have this cookie on your local machine, you have to manually set the API token that you generated via your developer dashboard:
heyVR.config.set( 'arena.token, 'YOUR TOKEN HERE' );
You can also set this value in the configuration file and feed it to the sandbox (see below).
Don't have an API token? You can generate a new one in the developer area, under the Settings panel.
By default, the SDK will try to infer the game slug automatically upon loading. You might need to override this, because your local game might have a different slug that the game you created on heyVR.
You can set this value by calliing the configuration object as below:
heyVR.config.set( 'game.slug', 'your-game-slug' );
You can also set both of the options above using the configuration file. To do so, simply download the sandbox.json file and set the value for game.slug
and arena.token
. Now you can feed this config file to the sandbox to modify the game slug.
New to local testing? You can read more about how to configure and use the sandbox on this page.
Once you have created at least a single room for your game, it will be available to use publicly. All of the arena features are available via heyVR.arena
object, which is provided by the SDK.
The lobby feature is mostly suitable for multiplayer games that support more than 2 players. If you're multiplayer game only supports 2 players (e.g. Chess-like games), scroll down and check out the Duel feature.
To create a new lobby, you can call the following method of the SDK:
heyVR.arena.createLobby(
options: {
room: string;
name: string | null;
team: string | null;
password: string | null;
}
) : Promise<Lobby>
We will fully cover the
Lobby
instance and its methods at the end of this article
room
: Required. The slug of the room defined by the developer when creating the room.name
: Optional. The human-readable name of the room that will be returned when listing lobbies.team
: The team that the player should connect to. Leave empty if you are using the default team configuration.password
: Optional. The player that creates the lobby can define a password for it.Promise
that will resolve to a lobby instance, or rejected in case of an error.To join an existing lobby, the developer would need the id
of that said lobby, alonsgide the room slug. The id
is a random unique identifier that is generated automatically when a new lobby is created. You can read more about the lobby instance later.
To join the lobby, you can call the following method:
heyVR.arena.joinLobby(
options: {
room: string;
lobby: string;
team: string | null;
password: string | null
}
) : Promise<Lobby>
room
: Required. The slug of the room defined by the developer when creating the room.lobby
: Required. The auto-generated id
of the lobby you are trying to connect to.team
: The team that the player should connect to, in case there are any teams defined.password
: Optional. The lobby password, if it has any.Promise
that will resolve to a lobby instance, or rejected in case of an error.Typically you'd call one of the methods above and then store the lobby instance that is provided to you via promise. In case you need to access the lobby instance somewhere else, you can call the following method to get the active instance:
heyVR.arena.getLobby() : Promise<Lobby>
This method will only work when you are already connected to a lobby, otherwise the promise will be rejected.
There can only be one active instance of the lobby class, so any call to this method will return the same instance of the lobby.
Promise
that will resolve to the lobby instance you are connected to, or rejected if you aren't connected to a lobby.If you would like to manually leave a lobby for any reasons, you can call the following method:
heyVR.arena.getLobby().then(
( lobby: Lobby ) => lobby.actions.abandon()
);
If the player was already in a lobby but lost connection due to any reason, they can attempt and reconnect to the lobby. To reconnect to a lobby, you can call the following method:
heyVR.arena.getLobby(
( lobby: Lobby ) => lobby.actions.reconnect() : Promise<Lobby>
)
reconnect()
method will return a Promise
that will resolve with a Lobby
instance if the player successfully reconnects, or rejected in case they weren't connected to any lobby to begin with.To get a list of available lobbies, you can call the following method:
heyVR.arena.lobbies( room: string ) : Promise<Array<AvailableLobby>>
room
: Required. The slug of the room defined by the developer when creating the room..Promise
that will resolve with an array of available lobbies, or rejected in case of an error such as an invalid room slug.The AvailableLobby
interface will look like the following:
interface AvailableLobby {
id: string;
name: string;
players: Array<string>;
private: boolean;
maxPlayers: number;
}
Once a player has joined a lobby, the developers will have access to an instance of the Lobby
object. This object offers various methods that the developer can use to manage the lobby and send/receive data.
Before we dive into further details, let's have a look at the overall lobby structure:
interface Lobby {
session: {
userId: string;
lobbyId: string;
username: string;
},
actions: {
abandon: () => void;
disconnect: () => void;
destroy: () => void;
listPlayers: () => Promise <PlayerList>;
reconnect: () => Promise<Lobby>;
},
commands: {
kickPlayer: ( username: string ) => Promise <string>;
banPlayer: ( username: string ) => Promise <string>;
unbanPlayer: ( username: string ) => Promise <string>;
},
communications: {
allChat: ( message: string ) => Promise<string>;
teamChat: ( message: string, team: string; ) => Promise<string>;
whisper: ( message: string, username: string ) => Promise<string>;
mute: ( username: string ) => Promise <boolean>;
unmute: ( username: string ) => Promise <boolean>;
call: () => void; // Not implemented yet
hangUp: () => void; // Not implemented yet
onMessage: ( callback: (payload: Object ) => void ) => void;
},
data: {
sendToAll: ( data: any ) => void;
sendToTeam: ( data: any; team: string; ) => void;
onReceiveAll: ( callback: ( data: any ) => void ) => void;
onReceiveTeam: ( callback: ( data: any ) => void ) => void;
},
state: {
current: Object; // Not implemented yet
onChange: ( callback: ( current: any, previous: any ) => void ) => void; // Not implemented yet
},
events: {
onDisconnect: ( callback: ( code: number ) => void ) => void;
onError: ( callback: ( code: number; message: any ) => void ) => void;
onPlayerSync: ( callback: ( list: any ) => void ) => void;
onMessage: ( callback: ( code: string, message: string ) => void ) => void;
}
}
We will explain each item further.
Each created Lobby
instance will have a read-only property accessible via lobby.session
. This property holds 2 read-only values:
The user ID of the current user. This is not the user's username, it's the session ID of the user. We will explain in details soon.
lobby.session.userId : string;
The publicly visible username for the current player can be accessed via the following object:
lobby.session.username : string;
If the user is logged in on heyVR, this will be their heyVR account username. Otherwise, a randomly assigned guest identifier.
The randomly generated unique lobby ID, used for tasks such as joining a lobby. You will need this later.
lobby.session.lobbyId : string;
Developers can perform certain action on a lobby. These actions are accessible via the lobby.actions
property. Currently, the following actions are available:
This method will allow you to list all of the players connected to a lobby that have joined a specific team:
lobby.actions.listPlayers( team: string | null ) : Promise<PlayerList>
If there's no team defined for the room, the team
parameter can be omitted. This method return a Promise
that resolves to an object containing the player details:
interface PlayerList {
[ sessionId: string ] : {
sessionId: string;
username: string;
team: string;
}
}
By calling the following method, you can completely destroy a lobby instance and force everyone to be disconnected. Only the player who created the lobby can perform this action:
lobby.actions.destroy() : void;
You can abandon a lobby permanently, which means you'll leave the lobby with no option to reconnect, and you'll be able to join/create a new lobby. The lobby will not be destroyed and players in the lobby can still continue to use it ( unless you're the last player in the lobby, which will also destroy the lobby after leaving ):
lobby.actions.abandon() : void;
If you want to temporarily disconnect from a lobby ( e.g. to fix connection issues ) you can call the following method:
lobby.actions.disconnect() : void;
If you're been disconnected from a lobby, you can try to reconnect by calling the following method:
lobby.actions.reconnect() : Promise<Lobby>;
This method returns a promise that resolves if the reconnection is successful, or rejected in case of errors.
Each lobby will be assigned an administrator whenever it's created. The player who creates a lobby will be considered its administrator. In case the original administrator leaves, the player who joined next will takes their place.
Admins can perform certain actions on lobbies, such as destroying it or banning players. Currently the following commands are available:
To kick a player from a lobby, the following method can be called:
lobby.commands.kickPlayer( username: string; ) : Promise<string>;
The kicked player can rejoin the lobby again. This action is useful to force the idle players to leave, as it's not a permanent ban.
username
: Required. The heyVR username of the player.Promise
that will resolve to a username of the kicked player or rejected with an object, containing the details of the error.The rejected object will have the following structure:
{
success: bool; // Whether kick was performed or not
username: string; // The username for the kicked user
message: string; // Response message,
code: string; // Debug code
}
The following debug code can be returned by the server. Feel free to hardcode these codes in your game as they won't change:
err_not_an_admin
: If the user sending the command is not the adminerr_cant_target_yourself
: If the user tries to kick themselferr_user_not_in_lobby
: If the given username does not exist in the lobbysuccess
: If the operation is successfulTo permanently ban a player from this lobby, the following method can be used:
lobby.commands.banPlayer( username: string ) : Promise<string>;
username
: Required. The heyVR username of the player.Promise
that will resolve to the username of the banned player or rejected with an object, containing the details of the error.The response object has the same structure of the kickPlayer()
method. An additional debug code can be returned by the server:
err_already_banned
: If the user was already bannedTo unban a previously banned player, the following method can be invoked:
lobby.commands.unbanPlayer( username: string ) : Promise<Object>;
username
: Required. The heyVR username of the banned player.Promise
that will resolve to the username of the unbanned player or rejected with an object, containing the details of the error.The response object has the same structure of the kickPlayer()
method. An additional debug code can be returned by the server:
err_user_not_banned
: if the user was not previously banned.Developers can use the Lobby
instance to establish communication between players in the lobby, inside the game. The following methods are currently provided for communication:
To send a message that is visible to everyone in the lobby ( no matter which team ), the following method can be called:
lobby.communications.allChat( message:string) : Promise<string>;
message
: Required. The message string to be sent publicly. Limited to 120 characters to prevent spam.Promise
that will resolve to the original message string on success or rejected with an object containing the details of the error.The structure of the rejected promise looks like the following:
{
success: boolean; // Whether the message was sent successfully or not
payload: string; // The original message sent by the user
code: string; // The debug code
reason: string; // Contains the human-readable error message sent by the server
}
The following debug codes can be returned by the server:
err_invalid_input
: If the input message format is invalid, such as not being a stringsuccess
: If the message is received successfullyTo send a message that is only broadcasted to the player's team members, you can call the following method:
lobby.communications.teamChat( message: string; team: string ) : Promise<string>;
message
: Required. The message string to be sent via team-chat. Limited to 120 characters to prevent spam.Promise
that will resolve to the original message string on success or rejected with an object containing the details of the error.The rejected promise object and debug codes are identical to all-chat.
A player can whisper to another player privately, without anyone else knowing. To send an in-game message privately to a user, you can call the following method:
lobby.communications.whisper( message: string, username: string ) : Promise<string>;
message
: Required. The message string to be sent privately. Limited to 120 characters to prevent spam.username
: Required. The heyVR username of the player to whisper to.Promise
that will resolve to the original message string on success or rejected with an object containing the details of the error.The the rejected promise is an object that is identical to all-chat. However, these additional debug codes can be returned:
cant_target_yourself
: If you try to whisper to yourselferr_target_not_in_team
: If the target user is not a member of your teamerr_user_not_in_lobby
: If the target user does not exist in the lobbyIf a player wishes to mute another player's incoming messages, the developer can call the following method on player's behalf:
lobby.communications.mute( username: string ) : Promise<string>;
username
: Required. The heyVR username of the player to be muted.Promise
that will resolve to the username of the muted player if the action is successful, or rejected with an object containing the details of the error.The promise will resolve to an object witht the following structure:
{
success: boolean; // Whether the operation was successful or not
username: string; // The username provided for the operation
code: string; // The debug code
reason: string; // If the operation has failed, this key exists and holds the error message
}
The debug code can contain the following values:
err_not_in_lobby
: If the username provided does not exist in the lobbysuccess
: If the operation is successfulTo unmute a previously muted player, the developer can call the following method on behalf of the player:
lobby.communications.unmute( username: string ) : Promise<string>;
username
: Required. The heyVR username of the player to be unmuted.Promise
that will resolve to the username of the unmuted player if the action is successful, or rejected with an object containing the details of the error.The promise resolves the an object that is idential to the mute method's result, however an additional debug code might be returned:
err_user_not_muted
: If the user was not muted to begin withNot implemented yet.
Not implemented yet.
To receive the messages sent by players using the previous methods, the developers can register a callback using the following method:
lobby.communications.onMessage( callback: ( payload: Object ) => void) : void;
An event handler can be attached to the lobby using this method, to capture all the incoming textual communication. The callback function receives an object with the following structure:
{
message: string; // The message content
senderId: string; // The sender's session ID
senderUsername: string; // The sender's heyVR username
whisper: boolean; // Whether this message was a whisper message or not
team: boolean; // Whether this message was sent using team chat rather than all chat
}
Developers can send/receive data to/from the game server. Developers can send public data ( update server's state ) or send data between team members.
You can send a piece of data to the server that will be broadcasted to other players in the lobby. You have the option to broadcast the data to everyone in the lobby, or only to the players on the same team with the sender.
To send a piece of data to the server that is broadcasted to everyone else, the following method can be called:
lobby.data.sendToAll( data: any ) : void;
If you wish to broadcast the data to a specific team, you can call the following method instead:
lobby.data.sendToTeam( data: any; team: string ) : void;
If the team
parameter is omitted, the player's team will be used instead.
data
: Required. A piece of data. Must be valid to be passed to JSON.stringify()
.team
: Optional. Only available when sending data to a specific team. Can be omitted to send to the player's team by default.A callback function can be attached to the lobby using this method, which will be called whenever a new data packet is received from the server. Depending on whether you want to receive the data that was send using sendToAll()
or sendToTeam()
, the following methods can be used:
These are the data that were broadcasted using the method in 5.1.1. To listen to these submitted data, you can register your callback as follows:
lobby.data.onReceiveAll( callback: ( payload: any ) : void) : void;
The callback method will receive the data that was sent to the server.
To receive the data that was submitted to this team ( the team that player is playing on ), you can register a callback using the following method:
lobby.data.onReceiveTeam( callback: ( { payload: any; senderTeam: string; } ) : void) : void;
The callback method will receive an object that has 2 keys:
payload
: The data that was send to the serverteam
: The team who has sent this dataEach lobby has an state object which is automatically synchronized to every player at the rate of 60 times per second. This state can be useful to synchronize the entire game for everyone.
Not implemented yet.
Not implemented yet.
Developers can listen to various events that might happen to the lobby, such as getting disconnected from the lobby or receiving an error.
To listen to whenever the player is disconnected from the lobby, the developer can register a callback function using the following method:
lobby.events.onDisconnect( callback: ( code: number ) => void ) : void;
The callback function will be invoked upon disconnection and will receive an error code. This callback will only register events for the client, not anyone else.
Developers can register a listener function in case an error has ocurred on the game server for an unknown reason, using the following method:
lobby.events.onError( callback: ( code: number; message: any ) => void ): void;
The callback function will be invoked in case an error occurs and will receive the error code along with the error message sent by the server.
Developers can listen to whenever a player joins or leaves a lobby. This is useful to update the list of current players in the lobby.
lobby.events.onPlayerSync( callback: ( result: Object ) => void ): void;
The callback will receive an object with the following structure:
{
event: 'join' | 'leave'; // Whether a user has joined or left the lobby
username: string; // The heyVR username of the said user
sessionId: string; // The lobby session ID of the said user
}
Developers can register a callback that is triggered whenever a system message is received from the server. The context is these messages are arbitrary, and mostly useful for debugging/development.
lobby.events.onMessage( callback: ( message: any ) => void ): void;
The callback function will receive an artbitrary message object that was sent by the server.
If your game's multiplayer mode only consists of 2 users ( whether co-op or competitive mode ), you can take advantage of Arena's Duel mode instead of implementing your own game server.
Duel mode will provide some valuable features, such as:
Duel mode will use lobbies under the hood to match players together, and then uses a PeerJS server to establish a direct connection between them.
Therefore, before using the duel mode, you'll need to create a new Room via the developer area. The room must have the following features:
default
and player amount of 2.Once you've created a room as mentioned above, you can simply call the following method to start an automatic matchmaking:
heyVR.arena.duel( room: string ) : Promise<LobbyP2P>
This method returns a promise that will be resolved to a LobbyP2P
object if the matchmaking is started successfully, or rejected in case of a network error.
As you might have noticed, Duel mode resolves to a slightly different lobby object, called LobbyP2P
. This object shares most of the methods with the Lobby
object ( described in previous sections ). This object has the following signature:
interface LobbyP2P {
session: {
username: string;
opponent: string;
isConnected: boolean;
},
actions: {
abandon: () => void;
disconnect: () => void;
destroy: () => void;
listPlayers: () => Promise <Array<string>>;
reconnect: () => Promise<Lobby>;
},
communications: {
chat: ( message: string ) => Promise<string>;
call: () => Promise<MediaConnection>;
onCall: ( callback: ( accept: () => void, decline: () => void ) => void ) => void;
hangUp: () => Promise<void>;
onMessage: ( callback: ( message: string ) => void ) => void
};
data: {
send: ( data: any ) => void;
onReceive: ( callback: ( payload: any ) => void ) => void;
};
events: {
onConnect: ( callback: () => void ) => void;
onDisconnect: ( callback: ( abandon: boolean ) => string ) => void;
onError: ( callback: ( error: PeerError<any> ) => void ) => void;
onMatchFound: ( callback: ( opponent: string ) => void ) => void
};
}
As you can see, the signature is almost identical to the Lobby
object. There are slight changes in the data
and communications
accessors ( for example, there's only one method for sending data, as there are no teams in Duel mode ).
As a result, if you'd like to access the LobbyP2P
instance, you'll have to call the following method instead:
heyVR.arena.getLobbyP2P() : Promise<LobbyP2P>;
We will not explain each method individually, as they behave the same way as the Lobby
instance. You can check out the Lobby
instance documentations above to know more about these methods.
The promise returned by the Duel mode does not mean that your match is ready. It simply means that the matchmaking request was successful ( imagine clicking the Find Match button in a game ).
Therefore, once you start a matchmaking process, you'll need to listen to specific events to be notifed whenever a match has been found or the users have connected to each other.
This event will be fired whenever a match has been found. This does NOT mean that the connection is ready to use. It simply means the system has found 2 players and will now try to connect them together. You can listen to this event the following way:
lobby.events.onMatchFound( callback: ( opponent: string ) => void ) : void;
Your callback will be executed once a match has been found. You'll recieve the username of the opponent player in the callback. This is a great way to improve your game's UX, by letting the players know their match is ready and now the system is trying to connect them together.
This is the main event you should be listening to. This event fires whenever the match is found AND a connection has been successfully established between 2 players. At this point, everything is ready and you can begin using all of the lobby's features.
You can listen to this event via the following method:
lobby.events.onConnect( callback: () => void ) : void;
If you'd prefer to completely take control over everything and handle the matchmaking yourself, we've got you covered even though we recommend using the Duel mode.
To connect to the P2P server and get a LobbyP2P
instance, you can call the following method:
heyVR.arena.p2p() : Promise<LobbyP2P>;
This method returns a promise that will resolve to a LobbyP2P
instance once the connection to the p2p server (not the target player) has been established.
Once you've got an instance of the LobbyP2P
object (provided by the promise), you'll need to listen to the onConnect
event before proceeding, to make sure you'll be notified when the target player connects. To do so, call the following method and pass your own callback:
lobby.events.onConnect( callback: () => void ) : void;
Then, you need to get your peer ID using the following method:
lobby.getID().then(
( id: string ) => {
// Share this ID with the target player
}
);
The id
will be a string with a length of 8 characters. You're in charge of sharing this code with the target player.
Once you've shared the id
value with the target player, you can use the same method that you used to host a connection, but this time you'll provide the id:
heyVR.arena.p2p( 'id-value-here' ) : Promise<LobbyP2P>;
This time, the promise will resolve when you've connected to the p2p server AND you've also connected to the remote peer. Everything is ready to use now.
In case of any error, the promise will rejected.