Jump to content

Building Multiplayer Games with Leadwerks

Josh

3,474 views

A new easy-to-use networking system is coming soon to Leadwerks Game Engine.  Built on the Enet library, Leadwerks networking provides a fast and easy way to quickly set up multiplayer games.  Each computer in the game is either a server or a client.  The server hosts the game and clients can join and leave the game at will.  On the other hand, when the server leaves the game, the game is over!

1491250148_client_server.png.df3eb728a7f045355f151f4ec1e5cc6a.png

Creating a Client

You can soon create a client with one command in Leadwerks:

client = Client:Create()

To connect to a server, you need to know the IP address of that computer:

client:Connect("63.451.789.3")

To get information from the other computer, we simply update the client and retrieve a message:

local message = client:Update()
if message.id == Message.Connected then
  	print("Connected to server")
elseif message.id == Message.Disconnected then
  	print("Disconnected from server")
elseif message.id == Message.Chat then
  	print("New chat message: "..message.stream:ReadString());
end

You can even send messages, consisting of a simple message ID, a string, or a stream.

client:Send(Message.Chat,"Hello, how are you today?")

There are two optional flags you can use to control the way your messages are sent.  If you specify Message.Ordered, your packets will arrive in the order they were sent (they won't necessarily otherwise).  You can use this for updating the position of an object, so that the most recent information is always used.  The Message.Reliable flag should be used for important messages that you don't want to miss.  UDP packets are not guaranteed to ever arrive at their destination, but messages sent with this flag are.  Just don't use it for everything, since it is slower!

When we're ready to leave the game, we can do that just as easily:

client:Disconnect()

A dedicated server does not have anyone playing the game.  The whole computer is used only for processing physics and sending and receiving information.  You can create a dedicated server, but it's better to let your players host their own games.  That way there's always a game to join, and you don't have to buy an extra computer and keep it running all the time.

Creating a Server

Your game should be able to run both as a client and as a server, so any player can host or join a game.  Creating the game server is just as easy.

local server = Server:Create(port)

Once the server is created, you can look up your IP address and ask a friend to join your game.  They would then type the IP address into their game and join.

The server can send and receive messages, too.  Because the server can be connected to multiple clients, it must specify which client to send the message to.  Fortunately, the Message structure contains the Peer we received a message from.  A peer just means "someone else's computer".  If your computer is the client, the server you connect to is a peer.  If your computer is the server, all the other clients are peers:

local message = client:Update()
if message.id == Message.Connected then
  	player2 = message.peer
end

You can use the peer object to send a message back to that computer:

server:Send(peer, Message.Chat, "I am doing just great! Thanks for asking.")

If you want to boot a player out of your game, that's easy too:

server:Disconnect(peer)

The broadcast command can be used to send the same message out to all clients:

server:Broadcast(Message.Chat, "I hope you are all having a great time in my cool chat program!")

Public Games

You can make your game public, allowing anyone else in the world who has the game to play with you.  You specify a name for your game, a description of your server, and call this command to send a message to the Leadwerks server:

server:Publish("SuperChat","My SuperChat Server of Fun")

All client machines, anywhere in the world, can retrieve a list of public games and choose one to join:

for n=0,client:CountServers("SuperChat")-1 do
	local remotegame = client:GetServer(n)
  	print(remotegame.address)
  	print(remotegame.description)
end

This is a lot easier than trying to type in one person's IP address.  For added control, you can even host a games database on your own server, and redirect your game to get information from there.



18 Comments


Recommended Comments

Would we call client/server Update() in a while loop until it returns nil with the assumption that there might be more than 1 message waiting for us?

Share this comment


Link to comment
1 minute ago, Rick said:

Would we call Update() in a while loop until it returns nil with the assumption that there might be more than 1 message waiting for us?

That is correct.

Share this comment


Link to comment

I like it.  Really curious where this ends up.  Interestingly, searching for "enet nat punch through" came up with a gamedev thread you started 6 years ago.

Share this comment


Link to comment

Any chance we can publish to our own masterserver? Like if I want to test internally or was making a private game?

Share this comment


Link to comment

How well will this work with writing unsigned char* data to a socket? Is this TCP or UDP? Does this have callbacks for dropped client/server connections?

Share this comment


Link to comment
41 minutes ago, Einlander said:

Any chance we can publish to our own masterserver? Like if I want to test internally or was making a private game?

Yes, you can upload the script on your server and use your own database.

14 minutes ago, martyj said:

How well will this work with writing unsigned char* data to a socket? Is this TCP or UDP? Does this have callbacks for dropped client/server connections?

You can use a bankstream to write any characters.  It uses UDP.  An event is received when a connection is dropped.  (There's not really any such thing as a "connection" in UDP, but it is "formed" and "broken" by Enet.)

Share this comment


Link to comment
Just now, Josh said:

You can use a bankstream to write any characters.  It uses UDP.  An event is received when a connection is dropped.  (There's not really any such thing as a "connection" in UDP, but it is "formed" and "broken" by Enet.)

By Enet are you referring to http://enet.bespin.org/?

Share this comment


Link to comment
5 minutes ago, martyj said:

By Enet are you referring to http://enet.bespin.org/?

Yes.  It's already built into Leadwerks and is used for the Lua debugger.  I just had to finished up a few details.

Originally, I had plans for a grand automagical system that would sync entities automatically, but I figured it was better to get this out there with pure user-defined messaging, and then build another layer on top of it, if needed.

Share this comment


Link to comment
Quote

All client machines, anywhere in the world, can retrieve a list of public games and choose one to join:


for n=0,client:CountServers()-1 do
	local remotegame = client:GetServer(n)
  	print(remotegame.address)
  	print(remotegame.description)
end

Shouldn't there be the name of the game somewhere in there, as well? Or do you just get a list of all servers and then have to read through the descriptions to see, if it is a server for your game?

Share this comment


Link to comment

I would think that because he's using client: the client is already connected to the server and so you are basically querying the server that you are connected to.

 

[EDIT]

Actually I was thinking you'd be connected to some master server but maybe not.

Share this comment


Link to comment
4 hours ago, Ma-Shell said:

Shouldn't there be the name of the game somewhere in there, as well? Or do you just get a list of all servers and then have to read through the descriptions to see, if it is a server for your game?

Yes, I corrected the code above.

33 minutes ago, DoomSlayer said:

This will come to LE4 or 5?

This will be in a 4.x version.

Share this comment


Link to comment

How long do you think this will be until it's in? In the next 6 months I think I'll have a need for this.

Share this comment


Link to comment
23 minutes ago, Rick said:

How long do you think this will be until it's in? In the next 6 months I think I'll have a need for this.

It's done, but not tested or documented much.

Share this comment


Link to comment

Sorry, I should have said when do you think it'll be pushed to release branch?

Share this comment


Link to comment
4 minutes ago, Rick said:

Sorry, I should have said when do you think it'll be pushed to release branch?

It will be included in 4.4 but probably won't be "official" until 4.5.

Share this comment


Link to comment

I really hate this. There is so much cool stuf you do with this and so little time.....

Maybe after some 'projects' are done, I will incorporate this in 'on the road again'. Would be fun to see multiple balls trying the same level.

Does this also work when you run an instance of the game as server and an instance as client on the same computer?

Share this comment


Link to comment
6 minutes ago, AggrorJorn said:

I really hate this. There is so much cool stuf you do with this and so little time.....

Maybe after some 'projects' are done, I will incorporate this in 'on the road again'. Would be fun to see multiple balls trying the same level.

Does this also work when you run an instance of the game as server and an instance as client on the same computer?

My chat example does exactly that.  Just connect to 127.0.0.1.

It's all documented and implemented at this point.  I added it into Linux this afternoon (not available yet).

Share this comment


Link to comment

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Blog Entries

    • By Josh in Josh's Dev Blog 0
      Textures in Leadwerks don't actually store any pixel data in system memory. Instead the data is sent straight from the hard drive to the GPU and dumped from memory, because there is no reason to have all that data sitting around in RAM. However, I needed to implement texture saving for our terrain system so I implemented a simple "Pixmap" class for handling image data:
      class Pixmap : public SharedObject { VkFormat m_format; iVec2 m_size; shared_ptr<Buffer> m_pixels; int bpp; public: Pixmap(); const VkFormat& format; const iVec2& size; const shared_ptr<Buffer>& pixels; virtual shared_ptr<Pixmap> Copy(); virtual shared_ptr<Pixmap> Convert(const VkFormat format); virtual bool Save(const std::string& filename, const SaveFlags flags = SAVE_DEFAULT); virtual bool Save(shared_ptr<Stream>, const std::string& mimetype = "image/vnd-ms.dds", const SaveFlags flags = SAVE_DEFAULT); friend shared_ptr<Pixmap> CreatePixmap(const int, const int, const VkFormat, shared_ptr<Buffer> data); friend shared_ptr<Pixmap> LoadPixmap(const std::wstring&, const LoadFlags); }; shared_ptr<Pixmap> CreatePixmap(const int width, const int height, const VkFormat format = VK_FORMAT_R8G8B8A8_UNORM, shared_ptr<Buffer> data = nullptr); shared_ptr<Pixmap> LoadPixmap(const std::wstring& path, const LoadFlags flags = LOAD_DEFAULT); You can convert a pixmap from one format to another in order to compress raw RGBA pixels into BCn compressed data. The supported conversion formats are very limited and are only being implemented as they are needed. Pixmaps can be saved as DDS files, and the same rules apply. Support for the most common formats is being added.
      As a result, the terrain system can now save out all processed images as DDS files. The modern DDS format supports a lot of pixel formats, so even heightmaps can be saved. All of these files can be easily viewed in Visual Studio itself. It's by far the most reliable DDS viewer, as even the built-in Windows preview function is missing support for DX10 formats. Unfortunately there's really no modern DDS viewer application like the old Windows Texture Viewer.

      Storing terrain data in an easy-to-open standard texture format will make development easier for you. I intend to eliminate all "black box" file formats so all your game data is always easily viewable in a variety of tools, right up until the final publish step.
    • By Josh in Josh's Dev Blog 1
      I wanted to see if any of the terrain data can be compressed down, mostly to reduce GPU memory usage. I implemented some fast texture compression algorithms for BC1, BC3, BC4, BC5, and BC7 compression. BC6 and BC7 are not terribly useful in this situation because they involve a complex lookup table, so data from different textures can't be mixed and matched. I found two areas where texture compression could be used, in alpha layers and normal maps. I implemented BC3 compression for terrain alpha and could not see any artifacts. The compression is very fast, always less than one second even with the biggest textures I would care to use (4096 x 4096).
      For normals, BC1 (DXT1 and BC3 (DXT5) produce artifacts: (I accidentally left tessellation turned on high in these shots, which is why the framerate is low):

      BC5 gives a better appearance on this bumpy area and closely matches the original uncompressed normals. BC5 takes 1 byte per pixel, one quarter the size of uncomompressed RGBA. However, it only supports two channels, so we need one texture for normals and another for tangents, leaving us with a total 50% reduced size.

      Here are the results:
      2048 x 2048 Uncompressed Terrain:
      Heightmap = 2048 * 2048 * 2 = 8388608 Normal / tangents map = 16777216 Secret sauce = 67108864 Secret sauce 2 = 16777216 Total = 104 MB 2048 x 2048 Compressed Terrain:
      Heightmap = 2048 * 2048 * 2 = 8388608 Normal map = 4194304 Tangents = 4194304 Secret sauce = 16777216 Secret sauce 2 = 16777216 Total = 48 MB Additionally, for editable terrain an extra 32 MB of data needs to be stored, but this can be dumped once the terrain is made static. There are other things you can do to reduce the file size but it would not change the memory usage, and processing time is very high for "super-compression" techniques. I investigated this thoroughly and found the best compression methods for this situation that are pretty much instantaneous with no noticeable loss of quality, so I am satisfied.
    • By jen in jen's Blog 0
      My small project will be called Foregate, it will be a dark medieval Diablo style single player action RPG.
      The graphics will be simple, no PBR, 256x256 map, reasonably low-res models.
      Camera style? Top-down-ish I think? Like in Diablo exactly - and because the camera is not directly in-front of the 3 models, I can get away with low-resolution assets - bonus. Also, with top-down view, I won't have to worry about high resolution sky-boxes. 
      What's my plan for this project?
      I plan to make this project as small and as simple as possible, possibly release it as open-source, and have fun with it of course.
      My previous experience with game development (1-2 years ago?) was amateurish I think, still is now. I want to give it a go again, this time with experience although my skill in C++ is not really that good? Maybe I can improve it in this project.
      More about the game
      The content is not set in stone yet but I have a general idea of how the mechanics is going to look and feel - Diablo-ish obviously. It'll have monsters (ancient & mythical probably), loot when killing a monster, gold as in-game currency, visual grid inventory, player stats (level, strength, agility, vitality, energy, &c.). 
      The game will be single-player. Possibly a coop multiplayer also? I don't have any interest in making massive multi-player. 
      I started my development yesterday with the basic preparations (setting up project environment, &c.), today I made my first step in developing the core components; worker class, game state, task class.
      I have a game state that keeps a single source of truth for the entire application; all game data will be stored in this class as "states". 
      I also have a "Worker" which will do the processing of tasks in the game.
      I also have "object" class, this can be a monster, the player, a weapon, a prop, or an NPC.
      So the idea is to have a CQRS type of interaction between the classes and the data. Any action in the game will be interpreted as "Task" for the Worker class. The worker class iterates through the Task. Tasks can be created by any class interfaced with the Worker class trough "addNewTask" and the new tasks can be of a certain type i.e.: ATTACK, IDLE, SAVE_GAME, EXIT_GAME, the new task will also have a payload data and it's processed according to its task type e.g. an ATTACK with payload "{ Damage: 10, Target: MonsterA }" will reduce the health of MonsterA by 10 - the worker class will change the game state; find MonsterA in MonsterState and reduce its health by 10. 
      I think it's advantageous to have this type of centralized module where all actions are processed; I can do all sorts of procedures during the processes, maybe debug data, filter actions, mutate payloads, and such.
      How much time am I going to put into this?
      A couple of hours a day for 3 days a week maybe.
      So it's all a rough sketch for now and it's heading the right direction. I'll have more to report later on. 

      This is Forgate Castle, minus the castle, in the map Forgate; the starting location for the player. The fortification will have merchants, and quest givers.
       
×
×
  • Create New...