Tutorial: Online Multiplayer with PodSixNet

Post your own tutorials, guides and demos.

Post » Thu Sep 02, 2010 3:36 pm

I plan on writing a server that is purely maths (made in code) anyway. So it wouldn't be too different writing the server purely in python.

Couple of questions about the tut.

1) It seems you use lists to give each player a unique ID (based on their location in the list). In a MMO style situation, you'd want to make sure that everyone has a unique ID that stays the same for the whole time they are online. Lists would work fine, however, when user 2 disconnects, we want the next user who logs in to take his place, this way we don't end up with a huge list (because the list re-uses empty entries). I assume this is the best way to do it? How do you do this in python, or is there a more appropriate way?

- also, wouldn't each client need a unique "ID" so you can send messages directly to certain clients from the server?

2) I couldn't quite work out how channels work. Can we send messages to all players on a channel? And can channel names be strings? Surely its important to ensure that we can send messages to certain groups of clients, but not everyone connected?

Just thought i'd ask so I understand these more fully. Thanks again scidave, seems to work really well!
B
2
S
2
G
5
Posts: 448
Reputation: 2,546

Post » Fri Sep 03, 2010 3:47 am

[quote="alee":ekplc39o]I plan on writing a server that is purely maths (made in code) anyway. So it wouldn't be too different writing the server purely in python.
[/quote:ekplc39o]

Maybe you missed my point, but I meant that if you're going to go through the trouble of duplicating all of Construct's helpful events and what not into python, why not just write everything from scratch to begin with?

The method of thinking you're taking will lead to road blocks down the road, primarily in the form of physics/extrapolation/interpolation. If the server-client movement code, for example, is not identical, your simulation will be off if you're using any kind of client-server architecture.

[quote:ekplc39o]
this way we don't end up with a huge list (because the list re-uses empty entries). I assume this is the best way to do it?
[/quote:ekplc39o]

Your "list" will only ever be the size of the amount of people connected. If you're worried going out of range on your indexing, perhaps you're not picturing it right in your mind.

First, you can use an ID generated by a storage medium (SQL, for example). Bear in mind the max int size within python is a signed long (for most distributions)... +/- 2,147,483,647. Try to imagine how long it would take to exceed that. At 10,000 connections a day, which ain't bad for most websites, that's 214,748 days until that number fills up. Or 588 years. Of course a malicious user who writes a connect/disconnect bot will make your server throw an exception rapidly.

Secondly, and probably more realistically, you should be using a guid. In which case, the concern isn't really valid to begin with. If you're going to send 4-8 bytes anyway, it may as well be a unique identifier. This might lend some security as well, if you use the first method and only that, objects will always be the same ID, which could give sniffers an advantage (hard to imagine how, but scripting would be much easier).

You might find a combination of the two to be the most appealing approach, though.

[quote:ekplc39o]
How do you do this in python, or is there a more appropriate way?
[/quote:ekplc39o]

Heh. You may be a little over your head at this point, no?

http://docs.python.org/library/uuid.html

Otherwise, in your class's add method, you could simply increment a private index value each time there's a connection. Off the top of my head... index = len(list)+1 would probably suffice.

[quote:ekplc39o]
- also, wouldn't each client need a unique "ID" so you can send messages directly to certain clients from the server?
[/quote:ekplc39o]

Well, PodSixNet is a connection based library... You'd just send directly to their connection. You might organize that into a set of containers (channels, for example), in which case, if you just want to talk directly to clients you could keep a "master" copy container (dictionary) for convenience. If I recall, that's how the author of PodSix does it, although he uses WeakRef (which is probably cleaner at the end of the day).

Try not to overthink it too much, in most cases, you'll be sending a response to the client directly so you'll know which channel to reply to at the moment of receiving their request (try to think of an event-based networking system -- this is how WoW does it).

[quote:ekplc39o]
2) I couldn't quite work out how channels work. Can we send messages to all players on a channel? And can channel names be strings? Surely its important to ensure that we can send messages to certain groups of clients, but not everyone connected?
[/quote:ekplc39o]

From a limited amount of looking at PodSixNet's innards, it does not do client channeling for you. I'm not familiar with the tutorial you're referencing, but client channeling is fairly easy to pull off. Use a dictionary (or some variety) if you wish to look them up by a string (probably pointing to a custom class you make which keeps references to the clients in said channel). This is where the various smart pointer classes in Python will be handy, since likely, you'll have references in multiple containers. Again, this all depends on how you design the data structures of your program.
B
3
G
2
Posts: 31
Reputation: 737

Post » Fri Sep 03, 2010 1:11 pm

[quote="catch22":q6aizjtt]
Maybe you missed my point, but I meant that if you're going to go through the trouble of duplicating all of Construct's helpful events and what not into python, why not just write everything from scratch to begin with?
[/quote:q6aizjtt]

There are so many events that need not be duplicated into the server. My game is just a graphical representation of some maths. (For example, Player 1 shoots player 2 and does 20 damage each second. The server will take 20hp from player2 each second, however on the client screen it will appear to be an epic battle with thousands of bullets or whatever. Another example is shooting a missile. The server will decide whether it hits and how much damage it will do and roughly how long it will take to hit player 2. However on the game screen the missile will fly and curve around etc.)

I won't have any physics. Interpolation is done client side (ofc) and If I choose to trust the client with his own movement, i'll have the server check if his movements are in the realm of possibility and disconnect him if not.

[quote:q6aizjtt]
....,647. Try to imagine how long it would take to exceed that. At 10,000 connections a day, which ain't bad for most websites, that's 214,748 days until that number fills up. Or 588 years. Of course a malicious user who writes a connect/disconnect bot will make your server throw an exception rapidly.

Secondly, and probably more realistically, you should be using a guid. In which case, the concern isn't really valid to begin with. If you're going to send 4-8 bytes anyway, it may as well be a unique identifier. This might lend some security as well, if you use the first method and only that, objects will always be the same ID, which could give sniffers an advantage (hard to imagine how, but scripting would be much easier).
[/quote:q6aizjtt]

This is exactly what I was talking about. I don't want the list to ever "fill up", it has to be robust so it cannot be exploited in this way. I figure when clients disconnect i'll just replace them in the list with "empty", then when a new client connects it either goes in the earliest empty slot, or if there are no empty slots, we'll add him to the end of the list.
Why exactly is it necessary to use a GUID/UUID? These IDs are only there so we can pick the correct player to update positions of on the client when we receive new movement vectors etc. I don't see why it needs to be anything other that just a short,simple value?

[quote:q6aizjtt]
Well, PodSixNet is a connection based library... You'd just send directly to their connection. You might organize that into a set of containers (channels, for example), in which case, if you just want to talk directly to clients you could keep a "master" copy container (dictionary) for convenience. If I recall, that's how the author of PodSix does it, although he uses WeakRef (which is probably cleaner at the end of the day).

Try not to overthink it too much, in most cases, you'll be sending a response to the client directly so you'll know which channel to reply to at the moment of receiving their request (try to think of an event-based networking system -- this is how WoW does it).
[/quote:q6aizjtt]

Ok this is fine. So basically PodSixNet doesn't handle any of that for you. We have to make our own "channel" system using a dictionary etc.

As to the response based communication. What about in WoW in the case of DOTs (damage over time attacks) for example. The server needs to keep updating the health of the victim even when there is no "event". Perhaps I misunderstand this part? But in these cases, the server will need to send updates to the clients of their health/damage done etc. even when there is no event or packet sent to the server from the client.

Thanks a lot for the help so far man! Hope i don't sound like I'm arguing, I often find challenging something I don't understand is the easiest way of understanding it :D
B
2
S
2
G
5
Posts: 448
Reputation: 2,546

Post » Fri Sep 03, 2010 2:31 pm

Hello. First off, (if I haven't already) I'd like to give my thanks for the tutorial. Anyways, I tried making my own chat application, but I can't get it to work. When I run the server app, type in the port, and click the 'Start Listening' button I get an error.

Traceback (most recent call last):
File "<string>", line 3, in <module>
NameError: name 'SysInfo' is not defined

When I click okay it's followed by another error.

Traceback (most recent call last):
File "<string>", line 1, in <module>
NameError: name 'myserver' is not defined

I copied and pasted the code from the example chat. The example .cap file works when I run it. That's what I don't get. I exported it to an .exe and imported all the necessary python modules, but I still received error messages. I installed the PodSix library properly too and replaced the .pyc files. Any idea/suggestions?
B
3
G
3
Posts: 23
Reputation: 946

Post » Fri Sep 03, 2010 8:00 pm

[quote="Jonathan5":1xggwffy]That's what I don't get. I exported it to an .exe and imported all the necessary python modules, but I still received error messages. I installed the PodSix library properly too and replaced the .pyc files. Any idea/suggestions?[/quote:1xggwffy]

Hi Jonathan. glad you liked the tut. Here is my guess at what is wrong.

For the problem with Sysinfo.. have you added the SysInfo object to your project and if you have multiple layouts, is it global?

For the myserver problem.. are you accessing "myserver" before you set the value of myserver? For example, the myserver.pump() call that needs to occur once each game loop. Trying to make that call before you do a "myserver = MyServer(localaddr = (host,port))" is going to throw that error. You solve this problem by either setting a group or a check on a global to make sure you don't do a myserver.pump() until "myserver" is set properly.

In general, whenever you get a "not defined" error it means you are accessing a variable before you have set it.


If you are still having problem after the above: Are you using Construct version .99.93 or later? If not, you should upgrade and try again as it may be a path problem.

Hope this helps!
B
8
S
3
G
7
Posts: 835
Reputation: 5,313

Post » Fri Sep 03, 2010 9:12 pm

[quote="scidave":2569axi3]For the problem with Sysinfo.. have you added the SysInfo object to your project and if you have multiple layouts, is it global?[/quote:2569axi3]

Doh! Can't believe I missed that. Thank you! I knew it was going to be something silly :roll:

[quote="scidave":2569axi3]For the myserver problem.. are you accessing "myserver" before you set the value of myserver?[/quote:2569axi3]

This is no longer a problem now that I have Sysinfo working.

I had another question (if you don't mind) and it could quite possibly be irrelevant with the network code, but I wanted to allow the client to press the Enter key as well as clicking the 'Send' button to send a message to the server. I thought a simple solution would be using the OR event in between: On talk button pressed and Enter key pressed. So it would look like this:

On talkButton clicked
OR
On key Enter pressed
connection.Send({"action": "message", "message": chatBox.Text})

But that seems to cause an infinite loop because the message continually sends. I know I could just make pressing the Enter key a separate event, but I was just curious about this OR operator. Sorry if that has nothing to do with this. I appreciate the help.
B
3
G
3
Posts: 23
Reputation: 946

Post » Fri Sep 03, 2010 10:07 pm

Good questions! btw... I really hope you finish your space game, because that is my #1 favorite genre. and online is even cooler!!
[quote="alee":13ofx2w7]1) It seems you use lists to give each player a unique ID (based on their location in the list). In a MMO style situation, you'd want to make sure that everyone has a unique ID that stays the same for the whole time they are online. Lists would work fine, however, when user 2 disconnects, we want the next user who logs in to take his place, this way we don't end up with a huge list (because the list re-uses empty entries). I assume this is the best way to do it? How do you do this in python, or is there a more appropriate way?[/quote:13ofx2w7]

A couple of clarifications... I don't use lists to give each player a unique ID. On the server side, I assign each player a playernum based on their location in the WeakKeyDictionary.
[code:13ofx2w7]player.Send({'action': 'number', 'num': len(self.players)})
player.playernum = len(self.players)[/code:13ofx2w7]

When a player (client) connects to the server the following code is called:
[code:13ofx2w7] def Connected(self, player, addr):
self.players[player] = True [/code:13ofx2w7]

This adds a reference to the player to the Dictionary. This reference is what is used to send data to the player:

[code:13ofx2w7]def SendToOthers(self, data, player):
for p in self.players:
if p != player:
p.Send(data)[/code:13ofx2w7]

Notice that in the above function, a check is made if the current player connecting is "yourself" and only sends to everybody else. Also note that the player number is not part of any of the sending of data from client to server or vice-versa. The player number is primarily used on the client side to determine which myplayer instance to control.

Also, each time a player disconnects their entry is indeed removed from the list/Dictionary, so the dictionary can never grow out of proportion:
[code:13ofx2w7]def DelPlayer(self, player):
statusText.AppendText("Player dead/disconneted" + str(player.addr) + "\n")
if System.globalvar("listenServer") == 0:
myplayer[player.playernum - 1].SetPosition(deadp.X,deadp.Y)
del self.players[player]
self.SendToOthers({'action': 'dead', 'player': player.playernum},player)[/code:13ofx2w7]

If you wanted [quote="alee":13ofx2w7]also, wouldn't each client need a unique "ID" so you can send messages directly to certain clients from the server?
[/quote:13ofx2w7]
You already can send messages to certain clients using the "player" weak reference. It is important to note that an instance of a client connecting to the server is a channel. The channel and player are one and the same. There is no private channel that multiple players exist on. For example, lets say in your game you want "Bob" to chat only with "Jill" instead of flooding the message to all players. When "Bob" sends his chat message to the server, he could tag it with an option that inlcudes "Jills's" name. Then the server would have a function called "Send to Player", which would take the name as input, figure out which player had a class variable name called Jill (for p in players... if p.name == "Jill", p.send...) and only send to that specific player.

[quote="alee":13ofx2w7]2) I couldn't quite work out how channels work. Can we send messages to all players on a channel? And can channel names be strings? Surely its important to ensure that we can send messages to certain groups of clients, but not everyone connected?[/quote:13ofx2w7]

Remember that a channel is a player connected to the server. I get what you are asking for though...you want groups of players. You can do this by creating additional dictionaries and adding the "Player" references to those dictionaries. Then when you want, let's say friendly factions/groups to communicate with each other, instead of sending to the "players" dictionary/list you instead send to "Faction1", etc... You could add an additional capability to each client to be able to create a new faction or group or whatever and you could even add some authentication word/string as well. So clients might have to connect with the name of the faction and the code word.

Keep in mind that PodSixNet is client-server architecture... all data must travel through the server. There are no peer to peer channels between different clients. You can however, create "logical" channels, but lets call them groups so it doesn't confuse the issue.

Now that Python support really rocks, I plan on updating the Tuts (sometime in the next couple months.. don't want to make any guarantees :-) and may add in a few extra features to the chat example to describe this.

There are some areas for abuse to look out for:

1. Right now, when a player connects they are a new "myplayer" instance on the client side. They send their player number to the server to update their position. However, there is no check on the server to make sure that the player they are updating is indeed themselves. A simple check to see if their self.playernum (which is only settable by the server) is equal to the "player" the incoming player want to control should solve this.
2. When a player is dead or disconnects they are removed from the dictionary on the server and packets no longer go to that player. However, their character still exists. This could cause a DOS if somebody repeatedly connected/disconnected creating a plethora of players. I can't remember why I didn't delete the player... I believe it threw off the numbering system. For example, lets say that you were player #3 and player #2 got killed. If you deleted the #2 instance of myplayer, I believe the #3 instance now becomes #2. Well, that is a problem because now player #3 doesn't have anybody to control because his instance is now #2. A couple of options: don't use playernumber tied to instances...this solves the problem of controlling the wrong or non-existant instance.. but then how do you control the instance? You need some type of mapping to an instance (for sake of code simplicity, unless I'm missing a better way). The second option is to update everybodies ID's when somebody dies, well actually this would have to happen in all cases since instance numbers change. When somebody disconnects or dies, you could send out an update to all of the players changing their global playernum (and thus which instance they control) and also send out a delete player "X". Some synchronization would need to be done to make sure this happened smoothly. I didn't want to overly complicate the Tut so I went with the simple/naive solution of copying the dead/disconnected instance off the playing board (which is not practical due to the DOS possibility in your MMO case).

Any ideas from Construct gurus (that bothered to read this far ;-) on better ideas to control a sprite instance?


Well that is enough of a brain dump for now. Please let me know if you have any questions!
B
8
S
3
G
7
Posts: 835
Reputation: 5,313

Post » Fri Sep 03, 2010 10:17 pm

[quote="Jonathan5":1o5o7jho] But that seems to cause an infinite loop because the message continually sends. I know I could just make pressing the Enter key a separate event, but I was just curious about this OR operator. Sorry if that has nothing to do with this. I appreciate the help.[/quote:1o5o7jho]

Try adding a "Trigger Once While True" system condition to that OR condition to see if that solves the problem. Cheers!
B
8
S
3
G
7
Posts: 835
Reputation: 5,313

Post » Sat Sep 04, 2010 3:46 am

[quote="alee":3j4sedgq]
There are so many events that need not be duplicated into the server. My game is just a graphical representation of some maths. (For example, Player 1 shoots player 2 and does 20 damage each second. The server will take 20hp from player2 each second, however on the client screen it will appear to be an epic battle with thousands of bullets or whatever. Another example is shooting a missile. The server will decide whether it hits and how much damage it will do and roughly how long it will take to hit player 2. However on the game screen the missile will fly and curve around etc.)

I won't have any physics. Interpolation is done client side (ofc) and If I choose to trust the client with his own movement, i'll have the server check if his movements are in the realm of possibility and disconnect him if not.
[/quote:3j4sedgq]

I've bolded the trouble points in your thinking. This is what I'm laboring to tell you. If you use Construct's built in events and functions on your client, in order to move things, time things, etc, and you do not do the same for your server, your simulation will simply not look right.

You're taking for granted these things, and I'm just trying to save you headaches later on down the road :)

1) Either use construct for server and client and not worry much about it,
2) Delve into Construct's source code for the simulation features you use on the client, but not on your python-only server
3) Write both the server and client code from scratch, and forego Construct entirely.

Truthfully, the second option isn't so difficult, and the third option seems silly since I know you're here to use construct to begin with. So it seems you should probably just embrace 1. Ideally the python only server seems sound, and it's not impossible, it's probably just more effort than it's worth for the scale of project you are working on.

[quote:3j4sedgq]
This is exactly what I was talking about. I don't want the list to ever "fill up", it has to be robust so it cannot be exploited in this way. I figure when clients disconnect i'll just replace them in the list with "empty", then when a new client connects it either goes in the earliest empty slot, or if there are no empty slots, we'll add him to the end of the list.
[/quote:3j4sedgq]

Try not to think so much in terms of slots, I believe a list is not the best way to organize this. You'd probably find it more convenient to use a dictionary, preferably one of the smart ones. A list might work alright for you, but I'd personally be after something more robust... probably a custom class built on top of a list or dictionary. I haven't really sat down and worked on it yet though in python... I need to do that :)

[quote:3j4sedgq]
Why exactly is it necessary to use a GUID/UUID? These IDs are only there so we can pick the correct player to update positions of on the client when we receive new movement vectors etc. I don't see why it needs to be anything other that just a short,simple value?
[/quote:3j4sedgq]

Well, it's not necessary, I'm just throwing ideas out there. Now.... 1 and 2,142,683 are essentially the same thing. They consume equal amounts of memory. So, if you're going to be using the same amount of memory/bandwidth, you may as well use a more robust numbering schema than just incrementing a variable.

[code:3j4sedgq]
import uuid
id = uuid.uuid4()
id.int
[/code:3j4sedgq]

Granted that's 32bytes long, you won't get a duplication in any kind of ordinary span of time (our lifetimes, easily). You could also save some memory by using just the first or last few bits of the ID (for example: id.node or id.time_low), though your randomization takes a dip (still, the chances will be quite tiny).

Even if you do get a freak duplication, you'd be checking it against active IDs anyway. If a duplicate exists, just generate another one. Couple this with a unique field in a SQL distribution of your choice.

[quote:3j4sedgq]
As to the response based communication. What about in WoW in the case of DOTs (damage over time attacks) for example. The server needs to keep updating the health of the victim even when there is no "event". Perhaps I misunderstand this part? But in these cases, the server will need to send updates to the clients of their health/damage done etc. even when there is no event or packet sent to the server from the client.
[/quote:3j4sedgq]

Events can happen in both directions, obviously. In the case of a DoT the client will receive an event that they have a DOT which does X damage over Y interval (display the debuff icon, duration, graphic, etc). The client could then simulate that effect with just the one packet send from the server. It could be broken up based on the tick duration too (multiple packets), but somehow I'd think they would resolve the damage ahead of time and let the client simply display the results over the course of the spell however it sees fit, rather than waste packet overhead simply on that.

Alas, WoW probably serializes a lot of data at regular intervals (watch the combat log, heh). So they might indeed elaborately break that data up on a per tick basis since data is streaming in that fashion. I've never sat down and sniffed out WoW traffic, so that's just an honest guess.

Here's a link that might interest you:

http://www.wowwiki.com/Events_From_Disassembly

[quote:3j4sedgq]
Thanks a lot for the help so far man! Hope i don't sound like I'm arguing, I often find challenging something I don't understand is the easiest way of understanding it :D[/quote:3j4sedgq]

Sure. No worries about the argumentative stuff, it doesn't really come off that way. I just hope I'm being helpful and not wasting precious posting space :)
B
3
G
2
Posts: 31
Reputation: 737

Post » Sat Sep 04, 2010 3:58 am

[quote:377nwjbm]...but then how do you control the instance? You need some type of mapping to an instance (for sake of code simplicity, unless I'm missing a better way). The second option is to update everybodies ID's when somebody dies, well actually this would have to happen in all cases since instance numbers change. When somebody disconnects or dies, you could send out an update to all of the players changing their global playernum (and thus which instance they control) and also send out a delete player "X". [/quote:377nwjbm]

This is all the ID talk that was going on between me and Alee. An ID would allow your client to know what object is being referred to when it receives updates from the server. When the client received a new object creation event/request it would assign itself the ID from the server.

Incorporating that into Construct in a reasonable fashion may be interesting, but I hadn't thought about it a lot. Does python have access to private object variables yet? Sadly, I have not yet begun on object replication at this time (nothing goes as planned with a newborn in the house! But I don't gotta tell you that :)

I did get distracted on a few side things though... A* pathfinding and box2d within Construct (I had done some box2d things prior to discovering Construct).

But, one of these days. . .
B
3
G
2
Posts: 31
Reputation: 737

PreviousNext

Return to Your tutorials & example files

Who is online

Users browsing this forum: No registered users and 2 guests