©2006 Jim E. Brooks http://www.palomino3d.org http://www.jimbrooks.org/sim
See also Palomino Design and Palomino Implementation.
Game elements were influenced by video games such as Blue Max (Atari/C64), Zaxxon, Microprose F-19, Terminal Velocity (DOS), Rogue Squadron, and by movies such as Star Wars, Iron Eagle, and Top Gun.
Palomino is the name of the core 3D renderer (3D engine). Game code is separated as "game modules".
The biggest issues facing a flight-simulator video game are the player getting lost and not knowing what the goals are. These issues can be solved by a map screen and a goals screen (they could be combined). The map screen would show a flight path and targets. The goals screen would show targets destroyed, list of accomplished mission objectives, etc.
Palomino is comprised of 3 major modules: the 3D engine, the base game module, and a specific game module. The modules are stacked in a hierarchy: higher modules can directly call base modules but not v.v. The idea is that base modules shouldn't know about the higher modules.
The interface is comprised of C++ classes, a few C++ global objects, and an event/listener mechanism. The event/listener mechanism allows base modules to indirectly call higher modules.
The engine supplies base C++ clases for the base game module. In turn, the base game supplies base C++ classes for specific game modules.The base game module, a specific game module, and the Engine can broadcast and listen to events using the Event C++ class templates. The mechanism serves as an interface between the modules.
The World object is a global. A game module derives from World class and constructs the World object. The engine doesn't know who makes or changes it -- the engine just renders it. The engine defers to a game module to populate a Quadrant with Objects. Several classes (Object etc) know about the World object and use it.
The engine calls the game module to perform animation.
Events are broadcast to listeners. Listeners register themselves with the Event object. Uninteresting events can be ignored by simply not listerning for them.
Obviously, the event/listener mechanism is well-suited for asynchronous events, but it's useful in other ways.
Events objects can be centralized in a C++ struct which makes them immediately recognizable as a high-level interface:
class EngineEvents { public: EventmEngineReady; // other modules should listen for this, not gfxsysReady Event mGfxsysReady; Event mExit; Event mAnimate; Event mTick; ... };
And it provides flexibility. The event sender doesn't need to know who is listening. This solves the problem in a module hierarchy where a base module can't know which functions to call in a higher module. In the following example, the base game module waits for the Engine to become ready. The Engine, which knows nothing about the higher modules, broadcasts an "engine ready" event.
gEngineEvent.mEngineReady.Listen( Listener_GameBase_EngineReady, start ); ... void Listener_GameBase_EngineReady( void ) { gGame.mInputQueue.Enable( &gJoy ); gGame.Start(); // engine is ready, let's play! }
State-machine code is written as a C array using macros provided by gfx_machine.hh.
*_T/F means to conditionally execute the instruction if the FLAGS register is true/false.
DEF_*() macros are helpers to define SM instructions (assembly-level).
MNEM_* are placeholders for instructions the SM compiler must resolve.
OPC_* are enums of SM opcodes (machine-level).
List of state-machine opcodes (very subject-to-change).
Documented here is a guide and the rules for writing state-machine code. For exact details, see gfx_machine.cc.
The macro expands OPC_PROC_INFO which has the procedure name and the compiled flag.
The macro expands a MNEM_PROC_END which is a terminator that stops the compiler.
If two procedures references each other (circular reference), the solution is to put each into a separate C++ source file and write extern declarations to each other.
C++ doesn't support a declaring an array with references to itself:
Machine::Opcode smproc[] = { // if ( cond ) // func1() // func2() OPC_EVAL, cond, OPC_JNZ, &machine[ index of OPC_EVAL,func2 ], OPC_EVAL, func1, OPC_EVAL, func2, OPC_RETURN };
This is solved by run-time compiling using named labels.
Machine::Opcode smproc[] = { DEF_PROC_NAME( "example" ), OPC_EVAL, cond, DEF_JNZ( "func2" ), OPC_EVAL, func1, DEF_LABEL( "func2" ), OPC_EVAL, func2 OPC_RETURN };
DEF_PROC_NAME(), behind the scenes, allocates a hidden element which is a flag to indicate if the procedure has been compiled. It expands to something like:
OPC_COMPILE_FLAG, false, //<-- compiled flag OPC_NAME, "example",
DEF_JNZ_REL() is a macro which expands OPC_JNZ_REL into the array following by a pointer to the label's string. The string must be compiled into a long (indexed by elements, not bytes) that is relative to the start of the procedure.
Compiling resolves label addresses which are the targets of jump instructions.
Compiling does these steps:
MNEM_LABEL, "again", OPC_NOP, OPC_NOP, // post-compile
MNEM_JMP, "again" OPC_JMP, (an address cast as a long), // post-compile
Machine::Opcode gSmprocPursue[] = { DEF_PROC_BEGIN( "Pursue" ), // every procedure must begin with DEF_PROC_BEGIN() DEF_CALL( sSmprocPursueSub ), // call a SM procedure DEF_FUNC( PursueIfRoll ), // call a C function, returns a bool into 1-bit true/false FLAGS register DEF_JUMP_F( "SkipRoll" ), // jump if flag is false (if PursueIfRoll() returns false) DEF_FUNC( PursueIfInlineWithTarget ), DEF_JUMP_T( "SkipRoll" ), DEF_FUNC( PursueRoll ), // state-machine is suspended OPC_SUSPEND, DEF_RERUN(), // re-run this procedure DEF_LABEL( "SkipRoll" ), // defines a label DEF_FUNC( PursueIfPitch ), DEF_JUMP_F( "SkipPitch" ), DEF_LABEL( "Pitch" ), DEF_FUNC( PursuePitch ), OPC_SUSPEND, DEF_RERUN(), DEF_LABEL( "SkipPitch" ), DEF_FUNC( PursueIfYaw ), DEF_JUMP_F( "SkipYaw" ), DEF_LABEL( "Yaw" ), DEF_FUNC( PursueYaw ), OPC_SUSPEND, DEF_RERUN(), DEF_LABEL( "SkipYaw" ), ... DEF_PROC_END() };
Palomino has its own GUI inside of a viewport.
GUI functionality:
The GUI is driven by a one-second timer and mouse input. It is rendered after the 3D scene in order to overlay it.
In order to co-operate with the application, the Gui class requires the application to install timer and mouse event handlers and pass those events to Gui methods. Like the 3D simulation code, the GUI code is split into gfxsys dependent/independent parts.
A "screen" is a set of GUI objects. The visibility of either an individual GUI object or a GUI screen can be turned on or off.
The font code originated from the open-source GL-117 flight simulator, and was developed into a Font and FontDesc classes (gfx/src/gfx_font*). Fonts are stored in TGA files and rendered using textures.
When the application creates a button, a callback function can be registered to be called when the button is clicked. Buttons are rendered in OpenGL 2D mode.
The Gui class is responsible for rendering GuiObject objects. GuiObject objects include buttons and messages (text that times-out). But text isn't a GuiObject, ie, Gui::Print() just renders text without creating a GuiObject for the text. To keep text showing, the application must repeatedly call Gui::Print() every frame. The reason is if a line of text were to persist across frames, the Gui class would have to return a handle which the application would later need to stop showing the text (this approach would be more complicated).
Since every aircraft needs its own independent chase-plane view, every Craft object has its own ChasePlaneCraft object. The ChasePlaneCraft class consists of a distance (from the target) and an ephemeral matrix derived from the target (which is synced with the target every frame if the chase-plane view is selected). Since some code manipulates Craft and Eye objects as base Dyna objects (eg sim_keyboard.cc), the Eye class has a dummy/NOP ChasePlaneBase class.
This game module is a mixture of influences from Blue Max and Zaxxon.
A 3D globe radar is meant to give the player an all-around perspective of the surrounding threats. It compensates for the limitation in reality of the user looking at a 2D computer monitor. Imagine a globe drawn as just two opposing circles: latitude and longitude. The player's craft is at the center. Since the player is viewing the 3D globe in front of him at the monitor, targets that are further behind the player's craft will be rendered larger as they're closer to him.
air shimmers/blurs from jet exhaust
afterburner animation
craft rocking up/down when movement begin
When a F-14 rolls sharply, the tailplanes very noticably rotate, much more than ailerons.
Last modified: Wed Aug 16 16:42:26 EDT 2006