|[ LiB ]|
In the SimpleMUD, the game logic was mainly stashed into a Telnet handler for each connection. I know I've mentioned this before, but it needs to be said again: That was not a very flexible design.
There's really no reason why a class that is designed to handle events inside the game should also handle getting input from a player. In a proper design, the core of the game should be abstracted away from the mechanism that handles connections, so that the design can be applied to many different situations. I've also mentioned that I would eventually like to add the ability to use protocols other than Telnet with the BetterMUD.
So, because of the need for flexibility, it makes sense to abstract the core physics engine into a module of its own. Therefore, the Game class controls everything that happens in the BetterMUD.
As you saw in the last chapter, many types of game actions can occur. When logic modules want the game to perform an action, such as moving a player, they notify the game engine by using an action.
The game engine checks to make sure that it can do what was requested , and then notifies all entities involved.
Characters don't know how to move around, pick things up, or perform other actions. Instead, all the physical logic is contained in one central location, the Game class.
Figure 15.1 should give you an overall sense of how the game data flows. The shaded square in the Logics section represents a Reporter object, which I cover when I go over the network stuff in Chapter 16.
At the core of the BetterMUD is the game module. This is a "controller" object. It essentially manages the physical part of the game, which consists of entities. Whenever the logical part of the game needs to perform actions on the physical world, it must send an action object to the game module. Or, if you prefer, the game module can instead say "This action is going to happen at time X," and add the action to the global collection of timer objects, which the game module also manages .
Due to the flexible nature of the engine, the game module not only tells entities what happened to them, but asks them if certain things can happen to them as well.
For example, you may want to have a huge gargoyle statue object in a room. On the one hand, you could simply say, "There is a statue in this room" in the description, but that can get boring. Besides, if you wanted the gargoyle to come to life, it would seem more realistic if the gargoyle were an item. So there is an item, and a player comes into the room. He decides to be cute, and tries picking up the gargoyle, which is too heavy to lift and is also solidly implanted into the ground. Obviously, you want to make absolutely certain that no- one can pick it up, even if someone manages to cheat and give himself more than enough strength to pick it up.
So for specified, attempted actions, the game queries the actors involved and asks each one if he will allow such an act to happen. This is the process taken to pick up an item, for example:
Query whether the item allows the player to take it.
Query whether the room allows the player to take it.
Query whether the player's current condition allows him to take it.
Transfer ownership of the item to the player.
Tell the room that the item was taken.
Tell the item that it was taken.
Tell all the characters in the room that the item was taken.
Tell all the items in the room that the item was taken.
Obviously, the first three actions are the queries. The game module first checks that the transaction can be completed, and then starts moving things around. Why do I need all those queries?
I've already outlined reasons for the first query; you'll often have items that you just don't want people to take, ever. The second query might confuse you though; why would a room care if an item is taken?
This is where your job comes in. As the MUD implementer, it's your job to brainstorm all the whacky possibilities that can happen. For example, someone casts a super gravity spell on the entire room, and while the room has super gravity, things are too heavy to be picked up.
Or maybe a part of a dungeon starts to crumble, and a precious item is now blocked by a large pile of rubble , which allows a player to see the item but not to take it. If you ask the room if it will allow players to take items, your game can handle these kinds of interesting scripts.
Of course asking players if they can take the item is a no-brainer; it's an easy way of checking to see if a player is carrying too many things. See the "encumbrance" module in Chapter 18 for implementation details.
After an event has been thoroughly queried, and the game module decides it's okay to carry out the event, the game module needs to tell every actor involved in the event that it happened. In the example I showed you previously, when an item is taken, the game notifies the following four groups of actors:
The room in which the item was picked up
The item that was picked up
Every character in that room, including the person who picked up the item
Every other item in the room
Again, you may be thinking, "Why does the room or the other items need to know when an item is removed?" If you're saying that, you're not using your thinking cap!
Be creative! Imagine...
You're Indiana Jones in some lost Aztec temple, about to claim the Sacred Statue of Pies for your museum, and as you snatch it, you realize all too late that it was booby-trapped! As soon as the room realizes you've taken the item, it sets off a trap that shoots poison darts at you, or maybe sets a huge rolling ball object in motion that will crush you to death if you don't get out of there in time!
Okay, so you can understand why rooms must know about items leaving; but what about all the other items? Why would they possibly want to know? Maybe you can imagine some set of magical items, a group of three or so, which erect a powerful force field in the room when they are all together. If you remove one... oops! The force field collapses, and that opens up the room to all sorts of evil creatures .
With a flexible scripting language such as Python embedded into the game, almost anything is possible. There is absolutely no way I could possibly tell you about all the cool things you can implement on your own; it's your job to find out. In Chapter 18, you'll get a brief glimpse into some of the things that can be done with the BetterMUD, but I assure you, the BetterMUD is designed to be taken far above and beyond what I give you.
I have to be honest with you here; I was having quite a bit of fun playing around with the BetterMUD before it was even technically a MUD! What you can accomplish is virtually limitless.
In Chapter 13, you saw that every entity was given functions to add and remove entities from itself. For example, rooms track the collection of all items, characters, and portals currently within them; characters track all of the items they have, and so on.
The game module manages these collections. When a player moves to a different room, the game removes him from the previous room and then places him in the new room. It's a pretty simple process overall.
There are a few things you need to keep in mind when performing actions, however.
The first is that in a highly flexible world, data corruption is very easy. You might have a script written by a new builder for the MUD, and he makes certain assumptions. For example, he may mistakenly assume that a certain character will always exist. So when the script runs, and it tries to get a pointer to the nonexistent character, it's obviously going to crash the script, and possibly cause something bad to happen.
To keep the data corruption to an absolute minimum, the game module always tries to perform the crashable parts of an action before it does any actual moving around.
For example, before anything is moved around as part of an action, the game module performs all the database lookups, and then performs all the queries to see if the action can be performed.
Once the lookups and queries are completed, the entity being moved around in the action is moved from one place to another, and the final act is to inform all the entities that are interested in what happened.
If any database lookup causes a crash, or if the queries cause a crash, nothing happens; the action fails, and whoever called the action should try to catch the exception.
The part of the code that performs the physical work should never cause a crash, simply because by the time the function gets to that part of the code, all the entities involved have been confirmed to exist.
Once the entity has been acted on, the function starts notifying everyone about the action, and each one of these calls may cause a crash.
Of course, if the calls do cause a crash, you're just going to have to live with it; at that point, the action has already occurred, and you're just informing players.
Once an item has been moved, you can handle logic crashes in a few different ways. You're going to be telling many items about the occurrence of an event, and each time you do so, the event may throw an exception at you. A really sturdy approach would be to catch every time an exception is thrown, log it, and then continue telling everyone else about it. However, I didn't take this ap proach; instead, the first time anything throws an excep tion, the entire function exits. The action still physically happens, but some entities may not be told about it. While this could destabilize things, you should realize that scripts should only fail during testing. Once you release a logic module to everyone in your realm, excep tions like this shouldn't happen anymore. Testing your scripts is a very important phase of building a MUD.
When you have a rogue script that crashes, what is the worst-case scenario? Maybe the script crashes every time it's run, and the only way to fix it is to delete the script's data and reload it from scratch. This can be a big problem, especially when that script was holding a lot of data. For example, the crashed script was holding data important to a quest.
It's just one of the things you have to deal with though. The great thing about a flexible MUD is that it's relatively painless to test; if a script screws up, you can simply reload it, and start testing again.
Because of this, I highly recommend that whenever you build new areas in the game, you restrict the new area and all its scripts to a certain group of test characters (don't use any real characters, because you may blow away your favorite character by accident ). Once you've tested an area thoroughly, you should then be able to integrate it into the rest of the game safely.
When I was testing some scripts for the BetterMUD, I really couldn't stop saying to myself , "Wow! This is so cool!" every time I found a tiny little syntax error in a script file. Instead of stopping the whole program, fixing it, recompiling it, reloading the game, and setting everything up again, I could just type /load <script> and it worked!
|[ LiB ]|