The XEvil Architecture Paper
Steve Hardt
Copyright © 1999,2000
This draft is not yet complete. However, it provides quite a
bit of information useful for understanding the XEvil source code and internal
architecture.
-
Object diagrams.
-
Overview of major modules.
-
Detail on many of the modules.
-
The outline is solid and representative of the final paper.
Updates will be posted on the XEvil Developer's Page, http://www.xevil.com/xevil/dev,
as they are written.
4/2/2000
Preface
This paper was originally a snapshot description of the internal architecture
of XEvil as it stands now. But, this view doesn't give the complete
picture. Like all complex systems, XEvil the result of a long incremental
and iterative process, specifically adding features, fixing bugs, removing
dead and duplicate code, commenting interfaces, testing, porting, and preparing
for beta and final releases. Design and redesign. Implementation
and reimplementation. So, I add a bit of historical context here
and there, describing the issues, decisions, and earlier versions that
led to XEvil its current state.
Although not strictly necessary, I recommend downloading a copy of the
XEvil source code to read along with this paper. This paper makes
occasional reference to specific files, data types, and code fragments.
Related Documents:
Contents
Introduction
XEvil is a cross-platform UNIX and Windows, client/server networked or
standalone, fast action explicitly violent game.
Physical vs. Logical Architecture
logical architecture, the abstractions and mechanisms that describe
the behavior of XEvil. Classes, interfaces. Algorithms.
The bulk of this paper concerns logical architecture, the abstractions
and mechanisms that describe the behavior of XEvil. The
physical
architecture of XEvil consists of the concrete and pragmatic decisions
about the source code itself, For example, the choice of implementation
language, set of supported platforms, allocating classes to files, directory
structure of the source tree, file naming conventions, packaging the source
for movement between developer machines and for distribution.
Object Diagrams
Notation
Very loosely based on Grady Booch's Object Oriented Analysis and Design
with Applications.
 |
X is-a Y |
 |
X has-a Y, one-to-one association, unless annotated with 'n' to indicate
one-to-n assocation |
 |
X has a tree of child classes |
 |
Abstract class |
 |
Existence of a number of child classes not in the diagram |
List of Diagrams
-Part I: Logical Architecture-
Modules
Diagram A: Toplevel
Architecture
Module Overview
Here we describe each of the major players in the XEvil internal
universe and how they interact with each other:
-
Main is the top-level, platform specific
timer-and-event loop. In addition to some platform-specific fluff
to satisfy the requirements of being an application, it creates the Game
object, then continuously feeds events to Game and clocks each turn of
Game.
-
Game is the top-level administrator and coordinator
of all aspects of XEvil. It creates the unique instances of World,
Locator, Ui, and Role and sets up the interactions among them. Game
is primarily a state machine. E.g. demoOn is the state
when the demo is playing at the program startup, gameOn is normal
game play, gameOver is the state between games, getBearings
is those few seconds at the beginning of each level where the game is frozen
so the user has a chance to see what's going on before all hell breaks
loose.
-
World holds the map of the XEvil world, floors,
walls, doors, movers (elevators), background images. When requested,
it draws rectangular portions of itself.
-
Locator, the Object Locator, manages the
set of all Physical objects. Locator drives all interactions among
distinct Physical objects, e.g. collision and proximity detection.
-
Physical is the root of a deep class hierarchy
of all physical objects. This includes internal nodes, abstract classes
such as Creature, Weapon, and Item, as well as leaf nodes, concrete classes
that actually appear in game play such as Ninja, Chainsaw, and MedKit.
We refer to the latter subset of concrete, leaf nodes as Actual.
-
Intel (no reference to the chip manufacturer)
is the tree of intelligence classes. A Physical object may have a
controlling Intel object. Both human and machine controlling intelligences
are represented as children of the Intel class.
-
Ui is the platform-specific user interface of
XEvil. It owns the windows and widgets visible to the user. Ui drives
all toplevel drawing requests and updates the screen each turn.
-
Role enacts all behavior with regard to the
Game's role in the current client/server network topology. Role has
three concrete subclasses,
StandAlone,
Client, and Server.
-
Utility Classs for geometric primitives, timers,
containers, wrapping c runtime libraries, string manipulation, and debugging.
Roughly partitioned into Area for rectangular regions, Coord
for geometric primitives, and Utils for the remaining utilities.
Main
Main runs the top-level event loop. It creates the Game object and
feeds events to it.
Create Game object. Then, until the Game quits:
-
Game::pre_clock(). All drawing here.
-
Empty event queue, passing event each to Game::process_event().
-
Game::post_clock(). All game logic for one turn of play.
-
Game::yield(). Throw away time since end of previous yield().
Watch for incoming network data, if appropriate. Implemented together,
using select().
In order to respond to user key events as quickly as possible, we empty
the event queue immediately before performing game logic. On most
machines, drawing will take the bulk of the time. And, on machines
with very fast graphics, throwing away time will be largest. So,
we are careful to not to put either of these between emptying the event
queue and performing the game logic turn.
On hindsight, <pre_clock(), yield(), events, post_clock()> might
have given better results. For a Server, this would avoid a
drawing cycle between incoming Client commands in yield() and the game
logic in post_clock(). This would also put drawing right before yield,
so the user would see the most up-to-date picture while time is being thrown
away.
Game
-
state machine and timers
-
parse and interpret command line argument
-
demo at program startup
-
create World, Locator
-
create Ui if needed
-
create Role
GameObjects
Beginning of each level, choose set of weapons and items for that level.
Tries to keep that set of weapons and items available. Originally,
counted held weapons/items. But, this led to hoarding. Instead,
only count the number of available weapons/items. Made it much easier
to get weapons/items. So put hard limit on the number of items and
number of items of each class each player could hold. So players
can't wait around at the end of a level picking up 50 MedKits.
Very extendable, asks the Locator for a list of all Physical classes
that have the potentialWeapon or potentialOtherItem property.
GameStyle
Diagram B: GameStyle
There are six "styles" of play in XEvil:
Levels
Kill all enemies on each level to advance to the next. Except, every
five levels you must complete a scenario. A scenario is a mission
with a specific goal, such as collect all the flags, find the exit before
the Aliens kill you, or survive an onslaught of rabid dogs.
Scenarios Only
No normal levels, you only play scenarios.
Kill, Kill, Kill
Everything attacks everything, machine players even attack each other.
Duel
Good for multi-player tournaments, each player has three lives.
Extended Duel
"Death Match" style for multi-player network games. Humans have infinite
lives and the game keeps track of the number of kills per player.
Training
No enemies, good for learning the controls (and debugging).
These sytles are implemented by the GameStyle object and its children.
GameStyle's public interface gives methods to set up the world map and
place human and machine players at the beginning of each level, decide
what weapons/items to allow, choose the background music, decide when to
advance levels, and decide when to end the game. There is a separate
child class of GameStyle for each game style, Normal, Levels, Scenarios,
KillKillKill, Duel, ExtendedDuel, and Training. (Difference between
Normal and Levels described below.)
Internally, GameStyle provides some helper methods to its child classes.
The most complicated being the logic to clean up weapons, items, and enemies
between levels. We want to get rid of the extra crap, but don't delete
human players, their weapons, items, or followers (Dogs, DoppelGangers).
KillKillKill, Duel, ExtendedDuel, and Training and fairly simple.
Random world, some specific number of human and machine players, start
new levels every certain amount of time to keep things interesting.
As you might guess, Scenarios requires the most logic to implement.
Each Scenario is significantly different than the others. The logic
to go "Kill the FireDemon" is not much like "Capture The Flag", run around
picking up flags So, we create another hierarchy, rooted with Scenario
(singular, not the same as Scenarios), with child classes KillTheFireDemon,
CaptureTheFlag, Hive, ThePound, JapanTown, etc. The public
Scenario interface is much like the GameStyle interface, logic to set up
the World and human and machine players, to choose background music, etc.
However, an instance of GameStyle, e.g. Scenarios, has a lifetime equal
to one game. While, each Scenario generally lasts only one level.
Assuming you beat the Scenario, the Scenarios object will randomly chooses
a different Scenario for the next level. If you fail the Scenario,
e.g. you fail to "Kill the Baby Seals", the Scenario object will make you
repeat the same Scenario until you get it.
The astute reader will notice there are seven different GameStyle classes,
yet only six styles of XEvil game play. There's some subtlty here.
Levels does not correspond to "Levels" in the game Ui. Selecting
"Levels" in the Ui, really gives you Normal. Although, "Scenarios
Only" does give Scenarios as you'd expect. Unlike the others, Levels
is only used indirectly by Game. Normal is like Levels, except it
throws in a Scenario every five levels to shake things up. Normal
does this by holding an internal instance of Levels and of Scenarios.
It delegates all behavior to its corresponding instance of Levels or Scenarios.
Normal has no logic of its own except to switch between using Levels and
using Scernarios.
Originally, the GameStyle logic was all mixed in with the Game object.
Lots of switch statements and that type of crap. Eventually, I got
sick of it and spent a week refactoring the game style code, pulling it
all out of Game and creating the separate object hierarchy for GameStyle.
It was well worth the effort , in avoiding bugs and simplifying later modifications.
World
World is a fairly reactive object, only responding to other objects' requests
of it. For example, the World does not enforce the constraint that
objects cannot move through walls. Physical objects must ask the
world if they are about to bump into walls, and take appropriate action
themselves. Within the time frame of a single level, the World map
is immutable. The big benefit here is network play. The server sends
the map once beginning each level, then never worries about it again.
Movers, or elevators, are the exception to both the reactive and immutable
nature of World. Movers move of their own accord and clearly have
continuous changing state. On hindsight, it would have been simpler
and cleaner to implement movers as Physical objects instead of the World.
To make drawing and network play work, I ended up having to implement a
Physical class, PhysMover, to reflect the state of each Mover.
In the initial design stages of XEvil we went back and forth
between having separate or unified representations for the components of
the world and physical objects in the game. On one hand, it would
be nice to have one common superclass for all entities appearing in XEvil.
But, there are some very big differences between components of the world
and other objects. The blocks in the World are very numerous and
generally immutable, while other objects are less numerous and have rapidly
changing location and other state. So we decided on a we decided
that
Maze algorithm, and specific mazes. Would like to provide a more
general interface for Scenarios to influence the world-generation algorithm,
so we could take all scenario-specific information out of World and contain
it all in the Scenario classes where it belongs.
See the Physics section for details on gravity and keeping players from
going through walls.
UnionSquares, Themes, Movers.
Physical Tree
Diagram C: Physical
Tree
Root is Physical, leaves are Actual.
contexts, Storing arbitrary parameters in easy to change form.
Nested.
PhysicalContext used as meta-data for objects, described in Locator.
Calling up the tree and _ methods.
There are quite a few classes in the tree of Physicals, we will only
mention a few of the key ones. Read the source, or look at classes.txt
for the full tree.
Physical Root
-
Current and next state, clock(), act(), and update()
-
is_* for reflection nice to have more general mechanism
-
Brief list of
-
pointer back to World and Locator
-
area
-
id assigned by Locator
-
mapped
-
draw()
-
die()
-
health
-
mass
-
collidable
-
corporeal and heat_attack()
-
heat, responsible for generating Fire objects
-
followers
-
Death, alive() == (get_health() >= 0), die(), kill_self(), set quiet_death()
and Game::off_clock_kill().
-
corpses, forward reference Creature
-
bombs
-
Serialization issues described in the Networking
section.
Moving
Moving introduces the concept of Dir (direction). Each Dir corresponds
to a potential sequence of animation frames. Since Dir exists in
the Coord module, each Dir is prefixed with CO_. Numbered in order,
from 0 to CO_DIR_MAX. There are two types of Dir. Pure directions
are the 16 cardinal directions, used for Shells and Missiles, CO_R to CO_UP_R_R.
For example, CO_R means straight right, CO_DN straight down, CO_DN_R down
right at 45o, CO_DN_R_R down right depressed 22.5o
from the horizontal (more right than down), CO_DN_DN_R down right depressed
77.5o from the horizontal (more down than right).
The other set of direction, non-pure Dir, are for Creatures with stances,
described in the next section.
-
avoiding walls, refer to Physics section
-
animation and Direction
-
Protection
-
Invisibility and masks
-
Velocity
-
Raw pos and offsets
-
Dead reckoning, forward reference
-
Drawing, MovingContext where you'll find most of the bitmaps specified
Use Lancer as an example of a typical Moving object, refer to the diagram.
Creature
The crux of Creature is Ability
Whereas pure Dirs are for missiles and objects that move in cardinal
directions, non-pure Dirs represent the movement and animation for Creatures,
e.g. running right, jumping left, climbing up ladder. The non-pure
directions are partitioned into seven stances:
-
CO_center - standing/walking on floor
-
CO_dn - crawling on floor
-
CO_r - crawling on right wall
-
CO_l - crawling on left wall
-
CO_up - crawling on ceiling
-
CO_climb - climbing ladder
-
CO_air - falling/jumping/flying, anything in the air
Each stance has several Dirs associated with it. CO_center has three
Dirs, CO_center R - walking right, CO_center_L - walking left, and CO_center
- standing facing the screen. The file classes.txt,
included in the top-level of the XEvil source code, has an ASCII art diagram
showing all the possible Dirs.
Similar to Stance and Dir are the concepts of Touching and Hanging.
As mentioned above, death does not correspond with object destrucion.
corpse,
follow movers
go through doors
interpret keys
Modifiers
-
modify one of a set of integer value
-
clocked each turn
Ability
Able to add/remove abilities at run time.
Solving the multiple inheritance problems, is-a vs. has-a. E.g.
Hero used to multiply inherit from Sticky, User, Fighter, and Healing,
which were all children of Creature. Now Hero singly inherits from
Creature and "has" the Sticky, User, Fighter, and Healing abilities.
Can only have at most one Locomotion and one Holder ability. Locomotion
for movement, Holder for interacting with Weapons and Items.
Object Locator
Abstractly, the Object Locator is the set of all Physical objects in XEvil.
Is the only thing with a list of all objects.
-
Locator Identifier (Id) acts like a "weak pointer", used to lookup objects
that may no longer exist. Similarly there is an IntelId.
-
collision and proximity detection, grid, see Physics section for results
of collisions
-
message queues
-
grid squares
-
class registry, create object by class identifier, get class meta-data.
Locator has registry of PhysicalContexts for all members of Actual,
i.e.
all the concrete Physical objects.
-
Human and Enemy registry.
-
teams
-
only add/delete objects at specific time
-
explicit_die()
-
Runs UNIX drawing algorithm describe in the
UNIX Specific section.
Locator::clock(), runs one turn of game play. Collision detection,
intelligence, act() and update() described in Physical,
adding new and deleting dead objects, updating Locator grid, reincarnate
bringing human players back from the dead. Was getting to large,
so split into a number of sub methods.
Intel
Diagram D: Intel Tree
Strong distinction between Physical and Intel. Soul-swapper,
frog-gun, Transmogifier, can very easily swap the intelligence objects
at runtime
Human Intelligence
A Human Intel mostly just buffers commands from the associated Ui viewport.
Stats such as score, and player name lie in the Human object.
Association between viewport in the UI and Human object. So, viewport
follows the human player, even when switched into a new body. Viewport
takes a keyboard command from the user and forwards it to its associated
Human which sends it to the Physical itself.
Human players have multiple lives, the same Human object persists after
the Physical object dies. The same Human object is reincarnated in
a new Physical body.
Artificial Intelligence
All Intels that inherit from Machine
strategy as finite state machine, filter_targets()
Enemy vs. DoppelGanger
Pet AI is enhanced DoppelGanger. 4 states, reltn between states
here and strategy
User Interface
-
Platform-specifc Chrome
-
Although World, Locator, and Physical contain the code to draw themselves,
it is the Ui that initiates all drawing operations.
-
Ui has no callbacks into game, the Game polls Ui every turn
-
Mention shared Ui code
UNIX Specific UI
Ui can be NULL, for command-line server, i.e. the "-no_ui" option.
Panel class
-
Not Motif
-
Bare minimum, ephasis on portability over everything
Viewport
-
large and small
-
scrolling and non-scrolling
-
refactoring operation
Windows Specific UI
Uses MFC.
Two UIs
-
Normal gameplay UI
-
Dedicated Server UI, doesn't draw anything, just gives server info.
Role
As much as possible, the Role class encapsulates all behavior related to
networking, i.e. the Game's "role" in the current client/server network
topology. Role has three concrete class children: StandAlone,
Client, and Server.
Game communicates with Role through Role's abstract interface, consisting
of three parts:
-
Access: Simple, Role returns some information about itself, e.g. whether
it is connected properly, the Role's preferred GameStyle, etc.
-
Notification: Game tells the Role about important events like "new_level",
"reset" (new game), "new_human_created", so that the Client/Server has
an oppurtunity to communicate with the Server/Clients across the network.
-
Timing: Two methods:
-
clock() gives the Role a chance to perform per-turn behavior, e.g. Server
tells the client what changed each turn, Client tells Server about movement/weapon
command issued by the user.
-
Game calls Role::yield at the very end of each turn, to throw away the
remaining time before the next turn. Since we are in a single-threaded
environment, yield() is the one place where Role can afford to block and
listen to the network for a period of time.
We describe Client and Server implementations of Role in the Networking
section. And, StandAlone is quite simple. The Access methods
are all one-liners. The Notification methods are stubbed out, since
there is no network communication. StandAlone::clock() does nothing,
and StandAlone::yield() simply sleeps for the remaining time in the turn.
Utilities
Geometric primitives, utils, coord, area
Utils
random, c-runtime wrapper, string manipulation, sorting,
Obviously, can't use Windows CRect, or X geometric primitives in XP
code. Make our own. Try to use as much as possible in front-ends,
especially in the interfaces. Would like to refactor MFC out of Win
front end more.
Coord
Pos, Size, Vel, Acc, Loc, Dim, Box, RoomIndex, Rooms. Conversions.
Area, originally intended to allow non-rectangular areas. "Region",
"Shape", all had namespace collisions with Windows or UNIX graphics libraries.
Transform2D used for autogenerating bitmaps, described in Graphics
section.
Stretched vs unstretched, comments to indicate whenever there is ambiguity.
Historical reason. Take advantage of factor of two,
Give pointer back to Moving and Creature for Dir, Stance, Touching
Utils is c-runtime wrapper
DebugInfo class, can still be used in a release build. Trivial
overhead, allows detailed bug reports from installed applications.
Physics
Whatever works. Have to deal with integer constraints.
All rectangle based. Store position, mass, and velocity.
Position is integer constrained, Velocity is floating point. Acceleration
is not explicity represented. Acceleration exists as an instantaneous
change in velocity in response to an event such as a collision or a user
keypress.
Two classes involved, Physical, i.e. the tree of all Physical objects,
and World. So, we have two types of collisions Physical-Physical
and Physical-World (world cannot collide with itself).
Physical-Physical collisions: Perfectly inelastic collision between
two point masses. Unmapped objects. Can set objects not to
collide with specific others, so you can't be hurt by own bullets. Objects
of greater mass can push others. Objects of zero mass don't push at all.
Was too annonying when bullets pushed you back.
Physical-World collisions: Avoiding going through walls.
Walking on floor and climbing on walls.
Gravity, Flying objects.
Graphics
Intertwined with the World and Physical objects.
Could have had abstract bitmap and pixmap objects, but difficult to
define an abstraction that encompasses all the differences between Windows
and UNIX. Multiple displays, Sharing DirectDraw surfaces on Windows.
Did not have all the information available at design time. So we
chose separate implementations of the drawing code
Xvars Abstraction Layer. More necessary on Windows because DirectX
is such a buggy unpredictable piece of shit, while Xlib is stable mature
technology.
xdata, CMN_*
Auto-generation rotations and reflections. Transform2D, PH_AUTO_GEN
in context. Reduced size of executable by a third. Greater
ease of modification.
Mask and Invisibilty. Shift bits of background.
UNIX Specific
Uses straight Xlib.
Drawing Algorithm
-
no backbuffer
-
transparency, arbitrary overlap
-
no flashing
-
layers
-
shadows, orphans
XPM
#included in the source. Auto-generated from Windows bitmaps, see
below.
Windows Specific
DirectDraw
-
Allows smooth-scrolling, high-speed
-
DirectDraw is buggy as hell, messy interfaces, implemented poorly on many
platforms, differences in behavior among video cards, does not fail gracefully
(crashes, corrupts color table). Interface changes used to intimidate
developers and drive marketing goals.
Bitmaps
-
256 color BMP files with a common palette
SurfaceManager
-
Can't afford to create a separate DirectDraw surface for each animation
frame of each character, so allocate several big rectangular surfaces,
and run layout algorithm to tightly pack all graphic frames on the surfaces.
-
Global allocation and layout of DirectDraw surfaces
Drawing Algorithm
Very simple, since we have DirectDraw to give us low-level blitting routines,
we simply redraw the entire screen each clock (25 fps). Draw
from back to front: background, then world blocks, then characters, then
text messages overlayed on the top.
Shared Graphics
Don't want separate set of graphics. Maintanence, easy of extension.
Network compatibility.
Could make custom file format. Instead used Windows BMP
files, tools already exist. Easier for artists to contribute graphics.
Tool to convert BMP files to UNIX XPM. Thus, do all graphics work
w/ Windows BMP files, and run the auto-generate for the UNIX builds.
Only have to run the tool when the graphics change.
Generate all graphics. Init all, then ask World to list all graphics,
Locator to list all Physical classes, then each Physical class to
list all graphics. Include transparency when appropriate.
Describe command line args to do so.
UNIX includes XPM files as
Networking
Describe the Server and Client implementations of Role, and the XETP Network
protocol.
Table A: Network Layers
| Layer |
Classes in Layer |
| Role |
Client, Server |
| XETP |
XETP |
| Stream |
TCP: NetInStream, NetOutStream
UDP: UDPInStream, UDPOutStream |
| UNIX Socket API |
<implemented by operating system> |
Streams
Don't reinvent the wheel. Use both TCP and UDP. Made simple,
platform independent, big-endian input and output TCP (NetInStream, NetOutStream)
and UDP streams
Methods like read/write_int(), read/write_char(), read/write_string().
Handles platform-specific differences in lengths of the C++ language
"char", "short", "int", "float", and in the signedness of "char".
UDPStream poorly named. Really deals with packets. For efficiency,
multiple XETP methods are buffered up and sent in a single UDP packet.
XETP (XEvil Transport Protocol)
Simple binary network protocol. Static methods in XETPBasic and XETP
to connect, disconnect, send/receive objects, commands, chat messages,
world maps.
Serialization
serialization (get_write_length() and write_to_stream()), and deserializaiton,
create_from_stream(). Used for network play, only writes enough so
that client can draw itself, emphasis is on minimizing the number of bits
stored. E.g. if we ever wanted to implement a save-game feature,
we would have to write enough to completely restore the state of each object.
Server
Server, non multi-threaded. List of Connection.
Adjust Data flow.
Disconnect clients after time.
Plays the game, like standalone
Role::clock() and yield()
Client
Just display what Server sends. Send commands back to server.
Dead-reckoning
Role::clock() and yield()
Network Compatibility
XEvil clients and servers can only connect if the same XETP version.
Startup, XEvil displays XETP version number. Generally, both client
and server need the same set of graphics and the same Physical/Actual tree.
Can add new scenarios, change client or server Ui, change game logic and
still maintain network compatibility. But, if you add a new character/weapon/item,
add new graphics, or add/change XETP methods, you must increment
the XETP version number.
Naturally, this influences our release management decisions. E.g.
XEvil 2.0, 2.01, 2.02 are all network compatible, XETP version 1.00.
We delayed any incompatiblity-inducing changes until XEvil 2.1. In
XEvil 2.1 we (will) introduce a number of new characters and graphics,
as well as some changes in XETP itself.
If necessary, we can introduce compatibility modes to clients and servers
to talk to other versions. Of course this is a giant versioning headache,
and we will only do it if users request it. Ideally, we'd have ways
of automatically downloading new graphics and characters from a server.
When a client connects, it compares its set of characters/world graphics
to the server and downloads any that it needs. Maybe for my next
game.
Server Ping
To enable web-based server monitors, XETP has a special method, XETP::SERVER_PING.
When an XEvil server receives this request, it replies with info about
the current state of the server, e.g. number of players, player names,
style of game being played, etc. Thus, a web-based application can
display a list of running XEvil servers, with continuously up-to-date information.
Sound
SoundManager, SoundRequest, and SoundNames
UNIX Sound
Dummy UNIX SoundManager.
An implementation of SoundManager for key, if not all, UNIX platforms
is exactly the type of thing that developers on the net could work on (hint,
hint, to any of you reading this).
A UNIX server must provide approprite sounds for Windows clients.
When a Client connects, it tells the Server whether it would like sound
events.
Windows Sound
Background music as MIDI files, sound effects as WAV. CD for background
music.
temp file system
-Part II: Physical Architecture-
Big constraint is the cross-platform requirement. Having separate,
independent sets of files for UNIX and Windows XEvil is unacceptable.
Costs of merging code and dealing with mistakes from incorrect merges would
quickly escalate out of control. We would be forced to chose which
version was the "real" XEvil, and then abandon the other.
C++
-
Restricted subset, no templates or exception handling because implementation
were not complete and consistent on all UNIX platforms when we started
XEvil, <DATE>.
-
Coding conventions, cross platform types.
-
Subject of a related paper, the XEvil Coding Conventions. <LINK>
Chose .cpp as the extension of choice because it works on Windows and
UNIX and there is no problem with case sensitivity. Originally used
".cc", then switched to ".C" as that seemed more common, then to ".cpp"
when we ported to Windows.
Comments and Interfaces
Comments on interfaces appear in .h files, generally not duplicated in
.cpp files. When trying to understand a method/function/variable,
you should look in the header file first. Comments in .cpp files
describe implementation details, algorithm, special cases, use of other
modules.
Update comments when code changes. If you update code, interfaces,
without updating the corresponding comments, the comments become useless,
perhaps even dangerously misleading.
In the case of a child class method overriding a superclass method,
the comment on the interface will generally appear in the top-most class
that first defines the method. No point in cut-n-pasting the method
description and risking the descriptions becoming out of sync. Child
overriding class methods will only be commented if there is something novel
or unintuitive about that method or its use.
Directory Structure
After porting XEvil to Windows, we went back and merged the Windows and
UNIX versions into a single codebase that could build on both platforms
-
There is no code in toplevel of the XEvil distribution, only some scripts
and documentation files. All C++ files are in subdirectories of "cmn",
"win32", and "x11".
-
"cmn" contains all the cross-platform code for XEvil, common to all builds.
-
"x11" and "win32" contain all platform-specific code.
-
Implementation of Main, in main.cpp on UNIX, in xevil.cpp on Windows.
-
xdata.h, xdata.cpp, and draw.cpp as described in the Graphics section below
-
subdirectories for graphics, sounds, and other resources.
-
The rest of the files implement the platform-specific user interfaces.
We make active efforts to suck code from the platform-specific directories
to the common directory. Code duplication is bad. Fixing the
same bug twice is bad. If a piece of code is only used by one platform,
but might someday be used by others, it should reside in the cmn directory.
#ifdefs
Platform-specific #ifdefs seem to be a necessary evil. In theory,
every difference could be hidden behind an appropriate cross-platform interface
or typedef, with platform-specific implementations. But, cost is
often prohibitive. Many point fixes where anything other than an
#ifdef would require significant costs in rearchitecting and reimplementation.
In particular, #ifdefs often need to be added late in the game as final
tweaks, when large structural changes are prohibitively costly and risky.
E.g. doing final builds, trying to make XEvil work on yet one more UNIX
platform.
So, there are platform-specific #ifdefs in the XEvil codebase.
We fight to keep them to a minimum, however.
Graphics
-
Intertwined with the World and Physical objects.
-
Xvars, xdata, CMN_*
-
xdata.cpp, xdata.h
-
draw.cpp
UNIX Graphics
bitmaps and gen_xpm directories
Windows Graphics
res dir, bind all graphics to executable
Sound
Windows Sound
MIDI files and WAV files are bound as resources to the executable.
"Bitmaps" Files
-
Originally to solve the problem of #including X Bitmap (later X Pixmap)
files for UNIX without having a single giant list of #include's for every
single bitmap in the game. Thus, the name .bitmaps
-
Evolved to become the location for the "contexts" for each class in Actual,
e.g. ninja.bitmaps has the contexts for Ninja. There is also a world.bitmaps,
ui.bitmaps, Perhaps ".context" or something similar would actually be a
better file extension.
-
Subdirectory of "cmn", "cmn/bitmaps/ninja/ninja.bitmaps".
-
Easy to locate and modify.
-
Extensible for large number of Objects in XEvil
-
Semi-readable by non-programmers.
-
directory for each Physical object
UNIX Platforms
-
The choice of supported UNIX platforms is simple: I make it work
on every damn UNIX machine I can get my hands on. (And some I can't,
thanks to people on the net.)
-
Do not have the resources to implement or support or buy a powerful software
runtime library. (We might have used Netscape's NSPR, had it existed
back then.) Our only hope for battling the complexity of the inconsistencies
between radically different UNIX implementation was to reduce library
dependencies to bare minimum.
-
UNIX XEvil requires a C++ compiler with the standard C++ stream libraries,
the basic Xlib libraries, and the XPM libraries (on hindsight, I wouldn't
have even used the C++ stream libraries. Not installed properly on
many UNIX machines). We don't use Xt or any Widget library.
I made a game before XEvil that used Motif, and ran into trouble getting
it to compile on all the machines I wanted. In those days, I didn't
have my own development machine(s), so I always had to deal with whatever
computers were available in various campus and workplace computer labs.
-
This strategy paid off in spades. I did all the original work on
Decmips machines, SGI, Alpha, and Sun workstations. Who would have
thought that Linux, FreeBSD, and the like would come out of nowhere bringing
UNIX to millions of inexpensive computers worldwide. Porting XEvil
to Linux was trivial work that gave a huge benefit in increased number
of potential XEvil users.
Windows Platforms
We support Window 95, NT, 98, and 2000. The build problem isn't nearly
as difficult as with UNIX, since they all use the Win32 API and the same
binary can run on all platforms. But we must still test on all platforms.
Like most Microsoft products, they claim to be compatible, but really have
all sorts of undocumented weirdness and differences that translate to great
pain for developers and users. <RANT>Microsoft: Fuck the user.
Fuck the developer. We're a monopoly and we own you.</RANT>
Win 3.1 is simply not worth the effort, all the 64K memory management
foolishness, poor development environment, crashes randomly, no DirectX.
And most Windows gamers are on Win32 or moving there soon.
Packaging and Installation
Binary
Single file executable for Windows and UNIX. Self-extracting EXE
for Windows. Standard compressed tarred, ".tar.Z", file for UNIX.
Should probably change to use GnuZip, ".tar.gz".
Made RPM file for Red Hat Linux.
Source
We use the same distribution file for UNIX and Windows. Simplifies
maintenance and distribution, and, most importantly, makes it easy to bring
changes back and forth between UNIX and Windows machines.
The UNIX Makefile has a target, distzip, such that "make distzip" will
package up all the source and create xevilsrc.zip. Windows has a
batchfile, dist.bat, that does the same thing.
All text files are stored in the zip file with Windows CRLF line break
conventions. The choice of UNIX or Windows conventions was largely
arbitrary, but we had to choose one. On UNIX, "make distzip" will
take care of the conversion in making the zip file. but developers must
be careful to "unzip -a" (autoconvert text files) when unzipping the file.
Some versions of unzip don't autodetect the XEvil binary files correctly,
so we include the script "unzipxevil" to unzip the source.
For XEvil 2.0 and 2.01 we created a "Source RPM" file for use on Red
Hat Linux. This may or may not be supported in the future.
-Appendix-
List of files
This gives the mapping from the physical architecture(files) to the logical
architecture(classes).
cmn directory (common to all platforms):
-
physical.{h,cpp} - All abstract nodes of the Physical hierarchy.
-
actual.{h,cpp} - All concrete nodes of the Physical hierarchy.
-
area.{h,cpp} - Rectangular areas.
-
coord.{h,cpp} - Geometric primitives.
-
game.{h,cpp} - Game, GameObjects.
-
game_style.{h,cpp} - GameStyle, all Scenarios.
-
utils.{h,cpp} - Global utilities, wrappers around standard C libs.
-
id.{h,cpp} - Identifier classes.
-
l_agreement.{h,cpp}- XEvil License Agreement, simple line-break parser.
-
locator.{h,cpp} - Object Locator.
-
neth.h - include appropriate header files for network code.
-
role.{h,cpp} - Role, Client/Server/StandAlone.
-
sound_cmn.{h,cpp}- Cross-platfor sound.
-
streams.{h,cpp} - TCP and UDP, platform-independent stream classes.
-
ui_cmn.{h,cpp} - Cross-platform user interface code.
-
world.{h,cpp} - World.
-
xetp_basic.{h,cpp}- Core methods for XEvil Transport Protocol.
-
xetp.{h,cpp} - Advanced methods for XEvil Transport Protocol.
-
bitmaps subdirectory - .bitmaps files for all Physical classes
x11 directory (UNIX/X only):
-
l_agreement_dlg.cpp - License Agreement dialog.
-
main.cpp - Main, outer-most loop.
-
panel.{h,cpp} - Simple ui widget classes, text labels, buttons, etc.
-
sound.{h,cpp} - Dummy SoundManager for compatibility
-
ui.{h,cpp} - User interface.
-
viewport.{h,cpp} - Large and SmallViewport
-
draw.cpp - Drawing code for World, Locator, Physical
-
xdata.{h,cpp} - Graphics utilities.
-
stdafx.h - Does nothing, for compatibility with Windows precompiled headers.
-
serverping.cpp - Separate utility for querying XEvil servers.
-
gen_xpm - generated XPM images
-
bitmaps - non-generated images
win32 directory (Windows only):
TODO
Would have done Differently
-
Better meta-data
-
rethink act()-update() logic
-
Do everything in double resolution, then round down when necessary.
-
No static sized arrays, use var-length list (PtrList) instead. Especially
in Locator.
-
Custom file format for characters and levels from files. Possibly
able to add new ones at runtime, or download automatically.
Evolution
See the XEvil timeline for specific dates.
Instances of large-scale code refactoring:
-
Created on UNIX
-
Port to Windows.
-
re-merge code, made cross-platform
-
introduce DirectDraw
-
Network play
-
Port Windows graphics to UNIX