Multiplayer tutorial

Simplified tutorial for creating multiplayer games using lua/love.  This is my answer to the tutorial desired by the love community: http://love2d.org/forums/viewtopic.php?f=3&t=3289

Just give me a simple framework that demonstrates how to do simple sending and receiving.
Summary
Multiplayer tutorialSimplified tutorial for creating multiplayer games using lua/love.
AssumptionsWhat is assumed about the programmer (you) writing a game.
Game descriptionWhat we will do.
Game architectureHow we manage things.
Game implementationWhat we use and how.
Game implementation - stage 1Create simple oneplayer game.
Game implementation - stage 2Change it to the multiplayer game.

Assumptions

What is assumed about the programmer (you) writing a game.

  • you know how to create simple game in love.
  • you do not want to know all the networking details, but to start coding your multiplayer game with minimal effort.
  • you are experienced enough to look at the source code to learn what you need.

So this is not for newbies, but if you are able to create a simple game for love, then you should be able to make it multiplayer (I hope so).  There is no code snippets in this tutorial, instead you should loog at the game source while reading this text.

Game description

What we will do.

We will create our game in 2 stages.  First it will be simple one-player game.  Then in stage 2 we will make it multiplayer.

So for the simple game in stage 1 we need

  • some random walls
  • each player is a randomly colored circle
  • each player can move in any of 4 directions
  • each player can “say” a message (which will be visible by everyone)

For stage 2

  • break code for client and server
  • client will send events to the server
  • server will respond to client’s events with changing game state and notifying all clients
  • server will detect when the clients disconnects and will remove its circle

Game architecture

How we manage things.

The game is quite simple.  To make things work better in the future, we will set a global table called GameState.  This table will keep all data required to get the current game state.  For our game it will contain

  • list of all players
  • list of all walls
  • list of messages

Each player will be drawn as a circle, so we don’t need to keep a shape of it.  But we need its position (X, Y) and color.  For the player we will make a class <Player>.  Each wall will be a randomly generated rectangle, so we need its position and size.  There will be a list of messages - who said what.  We will keep only the last 5 messages.

I will split the game code between main.lua (which will be mostly GUI- and mainloop related) and GameLogic.lua (which will keep all the game logic).

Game implementation

What we use and how.

To simplify things, I will use UDPROXY and UDPIPE, two libraries which I wrote to hide all the low level network operations.  I will also use class.lua, because I want my game to be written in some OO style.  The same OO style is used for UDPROXY, so you need it anyways.

Game implementation - stage 1

Create simple oneplayer game.  In this stage we will be looking at stage1 directory.

Create Player class

Siple class for each player.  First I will create the player class (look at Player.lua).  Player has its position and color (given or randomly generated).

Create GameLogic class

Then I create the GameLogic class (look at GameLogic.lua).  Important notes for GameLogic

  • internal state is kept in self.Players, self.Walls and self.Msgs tables, and get be get by calling getGameState()
  • some methods are “public API”, and will be called by GUI (the code in main.lua).
  • if the game state changes, I will call updateState method (for debugging only at this stage).
  • I wanted some computer player moving by itself, so in the update method the player called “auto” (if it exists) is moving every 0.5 second in random direction.
  • when there is a collision (with another player or with a wall), the player says something.
  • there is nothing GUI-related in this file.

Create main.lua

Creating the GUI/main loop.

This is quite standard.  Some remarks

  • I don’t want to hardcode the username in the code, so it uses USER environment variable.  So to set the player name, I would run USER=playername love . (I am using linux).
  • you can generate some events using function keys.  So F2/F6 to add/remove player auto, F7 to toggle caching (using FrameBuffers) etc.
  • you can send a message.  Just keep writing it and press ENTER.  (I know shift modifier does not work, but it is an example).
  • I am calling the Game:update() in love.update, but I don’t want to call it too often (for every frame), so I am throttling it to 0.005 sec.

To test this stage

$ cd stage1
$ love .

Move your player with cursor keys, press F2/F6 to add/remove “auto” player, keep pressing F3 for new random players.

Game implementation - stage 2

Change it to the multiplayer game.

At this stage we will look at the stage2 directory.

Additional libraries

I need some files, which I need to copy: UDPIPE.lua, UDPROXY.lua (the framework), dkjson.lua (json encoding), and optionally UDPIPEVERBOSE.lua (for debugging).

New server files

Now the game server will be another process, possibly run on different server.  So I create gameserver.lua file, which is an entry point (main loop) of my server.  It can be run in plain lua (with luasocket) or with love.

The server uses the same GameLogic.lua from stage1, overriding only its updateState method, so that the new information is sent out to all connected clients.

It uses also UDPIPE as a UDP-server, overriding it onNewMessage method such that it unserializes the JSON message, and if the method of the GameLogic exists (with a name of the command from the message), it calls it with provided arguments.  If the function returns a result, it is serialized and sent out to the sender (client).

New client files

I can not use GameLogic directly, because it is on the server now.  So I create ClientGameLogic.lua, which returns an instance of UDPROXY, and I modify it by overriding some methods (those not overrided will be sent to the server).  Because this is a proxy for the original GameLogic, I do not need to modify it too much.

I will use local copy of GameState, because my GUI code need a fast access to it (it draws players/walls every frame).  This local copy is initialized by the client with some dummy values, and then updated with values from the server.  I have modified the getGameState() method by adding a force argument - if there is no argument, then local copy is returned to GUI.  If there is a force argument, then the request is sent to the server.

Changed main.lua file

Here is the diff to the previous main.lua file:

@@ -1,5 +1,5 @@
 local lg=love.graphics
-local GameLogic=require 'GameLogic'
+local Game=require 'ClientGameLogic'
 username=os.getenv('USER') or 'guest'
 password='secret'

@@ -10,9 +10,7 @@

 function love.load()
   lg.setMode(MAXW, MAXH)
-  Game=GameLogic()
-  Game:login(username, password)
-  GameState=Game:getGameState(true)
+  Game:setUser(username, password)
   FB=lg.newFramebuffer()
 end

@@ -55,6 +53,7 @@
   -- Draw player name
   lg.print(username, MAXW-120, 10)
+  lg.print(string.format('Pkts: %d/%d', Game:getSentPackets(), Game:getReceivedPackets()), MAXW-320, 60)
   end

   local wascached=CACHED
@@ -82,7 +81,7 @@
   -- F1 - reinitialize the game
   if k=='f1' then
     Game:initialize()
-    GameState=Game:getGameState()
+    GameState=Game:getGameState(true)
     CACHED=nil
     return
   end
@@ -162,8 +161,19 @@
     dy=d
   end
   if dx~=0 or dy~=0 then
-    local nx,ny=p.x+dx, p.y+dy
-    Game:movePlayerTo(username, nx, ny)
-    CACHED=nil
+    if LASTMOVE then
+      LASTMOVE=LASTMOVE+dt
+      if LASTMOVE>0.05 then
+        LASTMOVE=nil -- reset last move timer. See also: ClientGameLogic:onPlayerUpdated
+      end
+    end
+    if not LASTMOVE then
+      local nx,ny=p.x+dx, p.y+dy
+      Game:movePlayerTo(username, nx, ny)
+      CACHED=nil
+      LASTMOVE=0 -- start last move timer
+    else
+      -- skip moving - don't do it too often
+    end
   end
 end

The list of changes goes like this

  • get Game instance from the new ClientGameLogic file instead of creating it in love.load as an inst ance of GameLogic.
  • move :login and :getGameState out of love.load to MyConnection:onConnect (check ClientGameLogic.lua).  This is because you want to sent your login and get the game state after you (re)connect to the server.  In love.load you set your default player name - and ClientGameLogic has to remember it, so I have added a new (local - not proxied) method :setUser.
  • added the number of packets sent/received (just for debugging)
  • for some cases when I want to be sure the state is updated from the server, added the force argument to :getGameState
  • I don’t want to send too many UDP packets to the server, so if the desired new position is different from the current one (i.e., you move the player), i call movePlayerTo(), and set a counter (LASTMOVE=0), which is increased every frame.  The client can not send another movePlayerTo() until the counter exceeds 0.05 sec or the updated player position came from the server (see ClientGameLogic:onPlayerUpdated() )

To test this stage

$ # terminal 1
$ lua gameserver.lua
$ # terminal 2
$ cd stage1
$ love .
$ # terminal 3
$ cd stage1
$ USER="player2" love .

Move your player with cursor keys, press F2/F6 to add/remove “auto” player, keep pressing F3 for new random players.

Simplifies calling UDPIPE, by serializing command names and its arguments, and sending it to the server.
Connects clients and servers with UDP.
Close