|[ LiB ]|
In many ways, the BetterMUD is simpler than the SimpleMUD, but in other ways, it is more complex. That may sound like a contradiction, but as I explain, it should make more sense.
If you've ever played an established MUD before, you know that the SimpleMUD is really dry. The most you can do is run around killing people and buying stuff. I got bored with it in a few days. As one of my friends affectionately told me, "The SimpleMUD is like a MUD with attention deficit/hyperactivity disorder (ADHD)."
As I write these words, SimpleMUD has consumed exactly 7 minutes and 59 seconds of CPU time over the past 33 days, and is using 0.1% of the system memory. The CPU time is kept really low through thread yielding, and the memory usage is kept low through the automatic management of STL containers. Believe meknowing about those tools is valuable .
As a learning experience, it was pretty cool, though. You learned to make a persistent world, and if you're new to MUD programming, you have probably never made a program that remained active for longer than a few hours. As I'm writing this, my version of the SimpleMUD (on Dune.net) has been running for 33 days solid, and that's a long time for a program to run, so I hope the SimpleMUD was a valuable learning experience for you.
Your players become especially angry when you make a change, hit compile, and the compile fails. At that point, you spend another hour or two tracking down the new bugs you just created, while your players can't log on, and they constantly message you asking, "When will the MUD be back up!?" Believe me, this will happen; I know from experience. It gets even worse if your players have paid to play on your MUD, because they expect a certain quality of service for something they pay for. Your players don't have much leverage if the MUD is free, though.
Another flaw of the SimpleMUD is its static nature. To change anything but the physical data, you have to stop the MUD, make the code changes, and then recompile it. This can become a significant problem if you continuously make changes, because it interrupts the community and angers your players.
So basically, all you can do to expand the SimpleMUD while it is running is to add new rooms, monsters, items, and stores, and that quickly becomes boring.
You can't do anything special with items, besides using them as weapons, armor , or healing potions. Monsters can't move around. There's no player versus player combat, and so on. I could make a list a mile long of the things that SimpleMUD is missing.
So, now you've just read that the SimpleMUD is too simple, but the BetterMUD is in some ways simpler than SimpleMUD. No, I'm not on drugs (though I may be crazy anyway). I'm being completely serious here.
Imagine this scenario for a generic MUD. There's a player who can create medical potions from herbs found in the ground in a forest. Since herbs are plants that grow abundantly in forests, you have a few options for implementation.
Thinking in SimpleMUD terms, the naive way of implementing this scenario is to hack a new check into the GameLoop module, and every 24 hours or so, it would fill up the room with 32 herbs (the maximum number of items allowed per room in the SimpleMUD). This method has several downsides:
All items remaining in the room at the end of the day are destroyed , since the SimpleMUD destroys the oldest items first when you go over the limit of 32.
Walking into the room will treat you to a description of "Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb, Herb". That looks annoying on this page, and that will certainly look annoying to your players.
Okay, so scratch the 24-hour refill method. Perhaps you could make a one-hour refill. Every hour, generate another herb? Or better yet, make a check to see if there is an herb in the room, every hour, and if not, then generate one?
As you can see, we're getting somewhere, but the process still doesn't correspond to how herbs grow. Obviously, new plants don't appear out of nowhere every hour, and even if this is just a game, it still looks cheesy. In reality, when you walk through a forest, there are hundreds of different plants, and that's the sense you'd like to convey in the game.
So an even better method would be to make this type of check on a room: "If player picks an herb, create a new one." Functionally, this is a good method in terms of what it accomplishes, until you start thinking about where you're going to put the code.
From my vantage point, it looks as if this code would go somewhere within the Game::GetItem function. (See Chapters 8, "Items and Players," 9, "Maps, Stores, and Training Rooms," and 10, "Enemies, Combat, and the Game Loop," if you are unfamiliar with the layout of the SimpleMUD.) I would like to strongly caution you against doing so, however.
What would the code look like? Perhaps something like this (in pseudo code):
if player.room == HERBROOM and item == HERB then insert new herb
That seems simple enough, at first. Then you realize: HERBROOM and HERB are hard-coded into the game now; and if you move the room, or change the herb to something else, your game still thinks that it needs to generate that item within that room. You may end up with some interesting side effects if you change your data around a lot.
At this point, you might be saying to yourself, "Well, it's only one item and one room; I can remember not to change them!" Sure, right now you remember. Next week you might remember as wellheck, maybe even next year. But what if you let someone else edit your files? Will he remember? What if you make more hacks like that in other rooms? Eventually you will have so many hacks that you won't remember which items and rooms should not be modified.
So you can see that hard-coding is a bad method after all. And you may have thought up a better alternative such as this: adding a piece of data to a room with this type of code "room respawns item X when picked up".
Hey, that works! Then in the game, you can check:
if player.room.respawnitem == item_you_are_picking_up then insert new item_you_are_picking_up
Awesome! Well, not quite. Feel free to hit your head on a table right now. I know this must be frustrating to see me shoot down so many different ideas, but we're almost there.
If you look closely at Figure 11.1 , you'll notice that its items are sorted by time. I did this for a good reason; you can reap significant optimization ben efits by storing items in a sorted manner. When the items are sorted by time, you can make certain assumptions about the data. For example, if you know that the top item has the lowest time, you know that every item below it has a higher time. If you check the registry at time 9,000 and see that the top item has a spawn time of 10,000, you know that you don't have to spawn anything, and you don't have to bother checking anything else in the registry. This speeds up operations significantly. Sorting is an expensive operation, but luckily the STL has a priority_queue container that has relatively quick inser tions and deletions (O(log n) times, for those of you who know about algorith mic analysis), and the container handles the sorting for you. I'll touch on this subject again when explaining the BetterMUD timer system in Chapter 15 .
What is the problem with this method? For starters, it adds extra data to every room in the game. How many rooms are you going to have that respawn items in your game? Probably one or two, maybe a dozen or two at most. So if you have 1,000 rooms, roughly 980 of them will have this extra piece of data that is completely superfluous. The rooms don't care about respawning items, and yet here they are, every one of them with an extra integer (4 bytes) and an extra 4 KB of memory. Yeah, yeah, I know, 4 KB is nothing these days, but bear with me for a moment.
Later on, you decide to make a special herbone that can cure only a special kind of illness (ignoring the fact that the SimpleMUD doesn't have illnesses), and due to its rarity, you need to make it respawn once a week or so. Frustrated yet? You should be.
So, you begin to figure out how to make this happen. Perhaps you could create a global registry of items that need to be respawned and a schedule for respawing. You could check this registry to see if things need to be spawned during the game loop, and if so, spawn them into the game. Figure 11.1 shows this process.
Now that you have a registry added, you need to figure out how items determine how much time they need to spawn. The best way to do this is to add yet another field to the room classa "how much time does X take to respawn?" field. That's another 4 KB of memory for a MUD with 1,000 rooms, and most of that 4 KB is wasted . (It's another 8 KB if you decide to go with 64-bit integers to represent time!)
On top of all that, every time you change the physics of an entity class, you need to somehow go through all your data files and add the extra field, or else your loading functions won't know what to do.
Now, what if you want to no, I'm kidding. I've taken this example far enough already, so there's no need to torture you with a thousand "what if's" and "should I's". I hope you see the point: The inherent flaw in the design of the SimpleMUD is that it cannot be extended without a significant amount of work.
A MUD is unique in being the one kind of game you can expand at small intervals. There's none of this "one expansion pack a year" stuff that you see with some MMORPGs, or "one expansion pack, total" that you see in nonpersistent-world games . MUDs are small communities in which people want to build their own virtual worlds and love jumping in and creating new areas or items whenever that meets their fancy.
Perhaps the best source of inspiration for MUD expansion occurs while you're playing the game, an idea randomly hits you, and you blurt out, "Hey! Wouldn't it be cool if... ?" and right then and there, you decide to start adding on.
This is the source of many problems in MUDs, however. So many of these "Hey! Wouldn't it be cool if...?" ideas are simple, little one-time deals. Maybe you want to add a simple little effect, but don't plan to use it anywhere else. I touched on this idea a little earlier; why would you add the capability to achieve one minor effect to every object in the game, when most of those objects never need it? When you design like that, you're wasting your time. You have to add these little of snippets of code all over the place, and that's just ridiculous.
To make a flexible system, you need to separate the physical aspects of the game from the logical aspects of the game. What is the physical? The physical aspect of the game is basically the rule set that describes what can happen in the game.
Items can be picked up and dropped by players.
Players can move from room to room.
The game produces "visual events" that players can see.
Items and players are created or destroyed.
Characters can see other characters moving around.
Those aspects are physical; they deal with what actually happens in the game. The logical side, on the other hand, controls the physical. You can think of the logical part of the game saying, "This is what happens when object X does Y." In the examples I showed you previously, the logical part of the game determines if and when herbs should be respawned, and the physical part of the game only cares about when the object is spawned. Figure 11.2 shows an example of abstraction for a normal room and for an herb-respawning room. This figure shows the benefits of having optional logic modules attached to the physical objects. Only items that require special behavior have logic modules attached to them. Note that logic modules may also need extra data. (Data is shaded in the figure.)
Because of the way normal rooms are designed, they are not involved with respawning and don't even know how to respawn. So, whenever an herb-room is told that someone picked up an herb, the logic module is consulted, and the module says, "I'm going to insert another herb into the room at time X".
To implement the "at time X" section of the logic module, you need a global timer registry, similar to the one I showed you in Figure 11.1 . I'll go into this more in Chapter 15 .
When a normal room is told that someone picked an herb, the normal room recognizes that it doesn't have a logic module, and therefore doesn't care what happens. Not only that, but since a normal room doesn't have a logic module, it takes up less memory. The herb-room's logic module manages all the special data it needs to know, such as when to respawn a new herb. The actual room has no reason to know about that data.
Implementing the physics of the game is fairly easy. You can do that in C++, and there's really no need to change the physics of the game often. So that's not going to be a problem.
The logic modules, however, are a different story. If you're still using C++, this may pose a problem. Sure, logic modules can be written in C++, and doing so greatly increases the cleanliness of your code, which is a good thing. But you still have that nasty little problem of not being able to add logic dynamically. This means that to add new pieces of logic to the game, you must stop, recompile, and run the code again. Unfortunately, these are the very procedures that are not advisable for MUDs, as I mentioned previously.
Loading data dynamically is easy, as you saw with the SimpleMUD, and as you have probably experienced in different contexts. Loading logic, which is always in code form, is a much more difficult process, but fortunately, there are a bunch of ways around this.
If you're targeting the Windows operating system, one popular way to load logic is by using DLLs, which are like EXEs, but you can load them at runtime, and call functions from them. This is a fast and easy way to support extensibility, but the cost involved is nonportability. Linux has no DLL files, so using this method limits you to Windows, which typically isn't as good a server platform as Linux.
Linux has expansion modules as well (SO files) but these can be a royal pain in the butt. To use SO files in Linux, you need to have your root administrator add the dynamic library to the list of globally available modules, which can be a big problem; root administrators are cautious about adding code when they are not sure what the code does. They don't want to inadvertently add a virus or a backdoor to the system, so you'll find many administrators unwilling to add new modules for you.
I mentioned earlier that the SimpleMUD has been running for 33 days. The longest my WindowsXP box ever ran was about 25 days. That's not to say that Windows is a bad server platform; it's actually pretty good. But Windows computers tend to be used for more than just server programs. For example, on the computer I am writing this on, I run lots of nonserver applications, like my word processor, audio player, compiler, IRC clients, instant messenger clients , and so on. All of these programs can have minor side effects that eventually destabilize the operating system. On a Linux server, you typically run only tried- and-true server applications, so they don't mess up the server. I would recom mend looking into a cheap Linux box to run your MUD server, and if you're really serious about running one, look into a shell service. (I use dune.net, and it's pretty good.) I list a bunch more in the Conclusion of this book.
Another option for loading logic is to use a scripting language. There are tons of scripting languages out there, each with its strengths and weaknesses. Some languages are nice and simple; others are huge and complex. If you're particularly devious , you may try embedding a full stack-machine language such as Java into your MUD, but that's going overboard in my opinion.
After playing around with a few languages, one jumped out at mePython. Lately, Python seems to have gained quite a large following, and for a good reasonit's simple and powerful.
Python was named after the British comedy troupe Monty Python . So, whenever you're working with Python, be sure to insert as many references to Monty Python as possible. Perhaps include some characters running around saying, " Help, help, I'm being repressed! " or " This is supposed to be a happy occasion. Let's not bicker and argue about who killed who!" Or even, perhaps, a riddle that involves knowing the air-speed of an unladen swallow.
The best thing about Python, however, is that it has a well-defined and documented C API, which means that it is incredibly easy to integrate with your application and easy as well to call Python scripts from your C/C++ program. Chapter 17 goes over everything you need to know about Python for this book. I'm not going to be able to include a comprehensive tutorial of the entire language, but believe me, you'll catch on quickly. The Python website (http://www.python.org) has a wonderful tutorial on the language, as well as a complete index of every built-in module. (There are tons of built-in moduleseverything from math to sockets, and strings to threading.) You can check ahead if you like, but until I cover Python in Chapter 17, I focus mainly on implementing the physical side of BetterMUD.
|[ LiB ]|