|
This document describes the code design for the BabbleMUD code base. Details of game logic (game play) are not in the scope of this document. The code design covers general principles and goals of the code base design, as well as implementation details (as they are hammered out.)
The Kernel
The core code base (the kernel) is meant to handle as little game logic as possible: it should be completely abstract from any particular game design. Rather, the kernel provides the mechanism for running game logic in a meaningful framework.
The kernel is multi-threaded. One thread controls networking and the passage of time. Another controls the game logic, and a final set of threads are used to perform potentially slow system calls such as DNS lookups. POSIX Threads are used to provide multi-threading. The 'main' thread of the program is the game logic thread.
Networking/Time Thread
The networking/time (NT) thread is in charge of processing all networking and passage of time. It maintains a list of active connections (descriptors) and a list of time-delayed events. These time-delayed events are expressed using the current time in milliseconds. In Unix, this is the actual time; Windows uses milliseconds since system startup. The difference is irrelevant; we only need a way to track a sequence of times in milliseconds relative to each other.
The select function is the core of time-keeping. It sets its time-out to the next event in the time list. This means that, if nothing happens, the server will remain idle until the event fires.
Whenever a descriptor has activity pending, it is serviced by the networking thread. After all servicing is complete, all timeouts are recalculated and the program waits on the select again.
A special descriptor is added to the set of selected descriptors: the wake-up pipe. Whenever an event is added to the queue, a byte is written to this pipe. This will then wake up the select, just as if input had been received on a connection.
The NT thread drives the creation of messages sent to the game logic thread. Whenever a message is created, it produces on the message semaphore.
Shared data
The NT thread touches the following shared data structures, which must therefore be synchronized:
- all connections, due to polling and servicing
- the wake-up pipe
- the timed-event queue
- the message queue
Game Logic Thread
The game logic (GL) thread has a queue of messages that are to be processed as fast as possible. Note that there are no time-delayed events in this queue.
The GL thread blocks on the message semaphore. Thus, whenever a message is produced by the NT thread, the GL thread will be woken up to process it. If for whatever reason the message is not consumed, the GL thread must 'give it back' by producing on the message semaphore again.
Shared data
The GL thread touches the following shared data structures, which must therefore be synchronized:
- all connections, due to receiving and sending text
- the wake-up pipe
- the message queue
- the timed-event queue
The Game Logic
BabbleMUD implements its game logic in Lua. This is to allow maximum flexibility across games; different codebases can use the same core and very easily implement their own game rules on top of or even replacing the main package.
Entities: Brief Introduction
The term 'entity' will be used throughout this section, so here is a brief introduction to how I use the term:
Game entity (GE): an object that has some existence in the game world. Specific examples are characters, objects, spells, projectiles, rooms. In other words, game entities are things that an omniscient being in the game could presumably ‘see’.
Meta-entity (ME): an object that has no real game presence. Specific examples are the web server, network connections, timed backups. Meta-entities cannot be ‘seen’ by the hypothetical omniscient being.
As a further clarification, meta-entities are in the core, whereas game entities are in the game. As such, MEs are principally stored in C++ objects whereas game entities have a Lua table representation.
Message-driven Game
As mentioned in the Kernel section, the BabbleMUD game logic is message(event)-driven. There is one inviolable rule: the game state changes if and only if a message is raised.
A message has the following components:
- a sender (originator)
- one or more receivers
- the message contents
These are defined as follows:
The Originator
The message originator can be a game entity or a meta-entity. As the name implies, the originator is the source of a message. Hence, there can be only one message originator. The originator is used in message-subscription; entities can subscribe to messages (optionally of a specific type) from a given (set of) originator(s).
The Receiver(s)
The receivers of a message are those who have an appropriate subscription (see below), either to all messages of the given type, all messages from the message originator, or messages of the right type from the originator.
The receiver decides what to do with the message, and hence any handling script is necessarily attached to the receiver.
Note that an entity can subscribe to its own messages, so the originator can be the same entity as the receiver.
The message contents
Every message has a type and a set of parameters. The message type is a string, typically somewhat self-descriptive (e.g. the type of a character dying could be named 'char_death'.) The parameters are a set of key-value pairs and are defined on a per-message-type basis.
Message Subscription
Messages are raised by originators and received by subscribers. As said above, subscription can be on the basis of all messages of a given type (e.g. an incoming connection handler), all messages from a specific originator (e.g. tracking an entity's activity), or messages of a specific type from a specific originator (e.g. a character listening to data received over a connection.).
Storing which entities hold which subscriptions is managed by the game core. There are two basic tables to this effect:
- By event type: this stores all subscriptions that are for all messages of a given type. This table is expected to be rather small.
- By originator: since the latter two categories of events are centered on an originator, this table is used for both of them. It maps originator to (the event handler) a receiver, with a possible type filter in between. This table is expected to be fairly large.
Finally there is a more advanced form of subscription, which is predicate based. This is to allow complex event subscription to be expressed compactly. For instance, we might want to subscribe to all 'death' events generated by all orc characters. So,
World Layout
Please see this document for more details on the BabbleMUD world layout.
Entities
The "entity" (for lack of a better word) is the core unit of the engine. It represents any game object: players, rooms, monsters, objects, containers, furniture... These entities are arranged in a simple inheritance scheme, e.g. a long sword is-a sword is-a meelee weapon is-a weapon is-a object. Inheritance allows entities to specialize generic types; a bastard sword and a broad sword might both be swords but might behave slightly differently.
Entities can either be instances, that is, actual manipulable game objects, or definitions, that is, everything needed to instantiate the object (similar to the SMAUG family prototype.) Each instance only has one definition, but a definition can be instantiated multiple times.
Entities are represented as XML documents. As such, their definitions are not controlled by the server, but rather by document description files that give types of nodes. The only exception to this is the top-most parent, "entity", which is defined by the engine and must be the parent of any in-game object node type.
Other XML node types have definitions as well, of course, which provide information on the attributes of the node, which are required and which are not, the types of the attributes, and whether a node of this type can have children, and if so, how many and of which type.
Internal data representation - e.g. an area file (or should this be player-manipulated as well? don't think so...) - do not inherit from the entity node and thus can not be "seen" by the players, but function the same in all other ways.
Entity tree View a sample entity inheritance tree.
Is this something we want? Does this make any sense? Is it solving any problems? Would each MUD game on the engine have its own inheritance scheme?
Event-Driven World
BabbleMUD is designed around a core game engine that handles primitive notions common to any MUD game, and a scripting engine that handles game-specific behavior. For example, the core engine is used to represent basic concepts such as an entity being inside another entity, room exits and room layout. It also lays down the basic event scheme, which passes control to scripts.
.......
Container
Messages
These message are sent to container entities (e.g. rooms, backpacks, player inventories...):
- can_enter (request): can the given entity enter this container
- enter (event): the given entity has entered the container.
- can_leave (request): can the given entity leave this container
- leave (event): the given entity has left the container
Problem: How do we pass parameters to an event? Most importantly, how does an event obtain the type of the entity node passed in? Since type checking is at compile time, do we need to get the type and then cast it? (icky.)
Or do we use the inheritance scheme here as well? Objects have to inherit from a "containable" node type, which provides a message interface for telling objects they've been put into stuff?
This is starting to have lots of probably-bad implications, like needing more and more "virtual" methods, multiple-inheritance...
Another alternative would to simply send the entity a message "you've been put into this container", which it may or may not understand/know how to process.
Object Messages These messages are used in object manipulation.
- can_equip (request): can the given entity equip this object
- equipped (event): the given entity has equipped this object
- can_remove (request): can the given entity remove (de-equip) this object
- removed (event): the given entity has removed this object.
Where are things like effects stored? Would those be in the equipped event? i.e. a sword adds 5 HP to whoever wields it. Would the equipped event add 5 to the HP of the wielder, and the removed event would remove the 5 HP? This would work, but we may want an external, core-engine controlled system of effects. This way, the effects can have timers, as well. For example, you equip that same sword; it creates a new effect, +5 HP, and tells the engine to add it to the player. But how would it know to remove the effect when removed? Perhaps, the engine returns an effect ID, which the weapon stores... and then uses to say "remove this effect."
This would be very good for potions, for instance: you drink the potion, and it creates an effect "restore HP", with parameters: howMuch=5, howManyTimes=4, delay=150 (ms). Thoughts?
How do we represent "equipped" objects? Are these a "fake" container?
Or, do players have slots - e.g. hands, torso, head - that can hold a given amount of items? (think item layers) How is this accomplished? I'm not fond of SMAUG's method of setting wear_bit to whatever location it's worn on.
- damaged (event): the object receives x damage (from y?).
- self_enter (event): the object is brought into a container
- self_leave (event): the object leaves a container
How does the object know if the container it entered/left is a player's inventory, a room, another object...? (see above about type checking)
- repaired (event): when the object is repaired (to what level of repair, by someone?)
- look (event): when this object is looked at (examined, studied, all the same... or is it?) by someone
- pushed, pulled (events): when the object is pushed/pulled (by someone?).
- used (events): when the object is used (by someone?)
What is the difference between pushing a button, and using a button?
- can_dial (request): can a person set this object to this state? (e.g. a dial of some sorts, a multiple-position switch...?)
- dialed (event): a person has set a dial to a certain position
How do players manipulate these objects? How do they know what states an object can be set to? Are states strings?
Weapons:
- can_fire (request): is this weapon ready to be fired at this target? -e.g. for a bow, does it have arrows? (is it the weapon's responsibility to check if the player is ready? e.g. the player is busy already?)
- fire (event): the weapon is fired (swung for a sword) at this target
Who is responsible for checking the player's ability to hit their target?
Character Messages - self_enter
- self_leave
General Entity Messages - begin: this entity was just created/loaded/etc.
- terminate: this entity is being destroyed
Too tired to do more now. :)
Are we approaching this the right way? Is this making any sense? Is it too complicated?
Are messages sent by the core? Or does the scripting language send messages? Or perhaps a combination of both? Combat seems like it would be largely handled by the script, so the scripts would need ways to make requests and send events.
|