This post originated from an RSS feed registered with Agile Buzz
by James Robertson.
Original Post: Making a MU* in 2 hours...
Feed Title: Michael Lucas-Smith
Feed URL: http://www.michaellucassmith.com/site.atom
Feed Description: Smalltalk and my misinterpretations of life
I've got a lot of friends who are really in to online role playing games. I used to be mad about them too - these days they're a random distraction. But the technology behind them is fascinating - imagine an Object Oriented system that auto-persists changes, has its own built in programming language that is reflective and is a multi-user system with security!
That's, at its heart, what a mu* is all about /for the programmer/. For the users - it's a game where you can create to your hearts content. Any way, back to the programmer :) - I wanted to see how easy it would be to make such a game in Smalltalk.
First off, peristence that doesn't get in the way - I used Prevayler - not your average Java-ish prevayler. This Prevayler is the one That Janko Mivsek had me write that is completely transparent to the developer (almost completely, so completely it doesn't work a mention).
This meant that to persist the game objects, alls I had to do was put them in a Dictionary at Prevayler.Repository root. Too easy. Okay so on to the game. One interesting design that most of these games has is that they're prototypical based systems. Each object has a parent and it inherits properties from that parent.
Not only that, but some properties are so named cmd_* so that they are a 'command' that can be executed. So a GameObject for me had a couple of instance variables: name, properties, contents, parent, location, owner
So we probably also want subclasses of that for Player, Room, Exit, etc.. I give a Player a password as well and I give an Exit a destination. Room's have exit's - so if you put an Exit in a room, instead of adding it to contents, it gets added to exits. Not much code to do that..
Right, so now that we have a work, we need to boot strap it. I'll leave that up to the listener - as it involves making a Player, a Room, etc... so lets allow people to connect to our server!
To do this we need to know a little bit about sockets and sockets in VisualWorks. For the server to know that somebody has disconnected, we need to have the server occassionally poll the user for activity. Since we can assume that this is a telnet connection, we can send the telnet commands 'DATA' followed by 'GO AHEAD'. This just two bytes: 255 and 249.
So lets open up the socket:
(server := SocketAccessor newTCPserverAtPort: 8889) listenFor: 1.
Then lets listen for connections:
listener := [[self listen] repeat] newProcess.
listener name: 'Game: Inbound Connections'.
listener resume
If we hold on to the listener and the server like this, we can then stop the server later. We can terminate the listener process and close the server socket. So what does listen do?
We put in the delay just to slow down the server. We don't want to get flooded with connections and we don't want to spend 99% of our CPU constantly checking if there's anyone there.
Once we have a new connection, we want to set up the Game and we also want to set up the KeepAlive. The first thing we do with the new connection is to tell it to 'GO AHEAD' so that the telnet negotiation process is terminated.
GameServer class>>newConnection: connection
| socket |
connection nextPutAll: (String with: 255 asCharacter with: 249 asCharacter).
connection upToEnd. "this is what the client has to say about its capabilities."
socket := GameSocket onConnection: connection.
self newSocket: socket
GameServer class>>newSocket: socket
| gameProcess keepAliveProcess |
gameProcess := [socket play] newProcess.
gameProcess name: socket name, ' - Game'.
gameProcess socket: socket.
keepAliveProcess := [gameProcess keepAlive] newProcess.
keepAliveProcess name: socket name, ' - KeepAlive'.
keepAliveProcess socket: socket.
keepAliveProcess resume.
gameProcess resume
Now we have two threads running in the system. One will service the users needs and the other will make sure the game keeps running and knows when the user has disconnected.
Here, the Process instance we're on is the Game thread. But the thread we're in is the keepAlive thread. Tricky eh? So that means when socket isClosed, we can terminate the game thread by going 'self terminate'. Better yet, if the game is being debugged and the game process ends up dieing, the keep alive can detect this with self isTerminated - and start a new thread to service the user - so the user isn't left with an unresponsive terminal.
From here's it's mostly academic - putting in commands, etc.. but HOW you do it next is interesting. I had two friends connected at this stage and I quickly bootstrapped a chatting program. Very shortly after that I started adding commands - while they were connected. I could get them to try out my commands (free testing!) as I changed them and added new ones. By the end of two hours we had rooms, you could move between them, chat, pose, look, set properties, so on and so forth...
The game is published to Public Store as MultiUserGame (Mug). It's not finished by a long shot - what it needs right now is security so that a game user can write Smalltalk and not circumvent security in (a) the server or (b) the game. This is in the works.. I'll write about that some more later..