ZDoom 1.19 offers a partial conversion of the Doom code to C++. As such, some functions and classes have been encapsulated in classes. This document tries to give an overview of those classes and how to use them. Command line parameters ======================= Previous versions of ZDoom kept the command line in the myargv and myargc global variables. This version uses a single variable, Args, of type DArgs. This class is defined in m_argv.h and implemented in m_argv.cpp. The three most important functions are: int DArgs::CheckParm (const char *check) const; Checks to see if a parameter exists. If it does, it's index number is returned. Use the GetArg() method to retrieve an arguments specified after it. char *DArgs::CheckValue (const char *check) const; This is similar to CheckParm(), but it returns the value of the argument immediately following parameter. If the parameter is not present, or it has no argument, NULL is returned. char *DArgs::GetArg (int index) const; Retrieves the value of an argument. This is similar to argv[index]. For the other functions, check the implementation in m_argv.cpp to see what they do. Error messages ============== There are two functions for reporting error conditions: I_Error() and I_FatalError(). Use the first for errors that can be recovered from. It will drop the user to the fullscreen console and print the message. Use the second for error conditions that are much harder to recover from. It will terminate the program and display the message. Console commands ================ Adding new console commands is now very simple, compared to previous versions of ZDoom. Registration is now completely automatic if you use the BEGIN_COMMAND and END_COMMAND macros defined in c_dispatch.h. A simple command could be implemented like this: #include "c_dispatch.h" BEGIN_COMMAND(hello) { Printf (PRINT_HIGH, "Hello, world!\n"); } END_COMMAND(hello) The parameter to the BEGIN_COMMAND and END_COMMAND macros is the name of the command, as typed at the console (in this case, "hello"). You should use the same name for both BEGIN_ and END_. You can access the arguments to the command using argc and argv. If you want to know who activated the command, m_Instigator is a pointer to the actor of the player who called it. Console variables ================= Adding new console variables is also similar. The c_cvars.h file contains four macros related to using cvars: CVAR (name, default, flags) Declares a new console variable with the name "name" and default value "default" and flags "flags". (See c_cvars.h for the different flags). An example: CVAR (var_friction, "1", CVAR_SERVERINFO); EXTERN_CVAR (name) Imports a cvar defined elsewhere. This just generates the code "extern cvar_t name;", but you should still use this macro. BEGIN_CUSTOM_CVAR (name, default, flags) END_CUSTOM_CVAR (name) Declares a cvar that performs some special action each time its value changes. You should use a compound statement between the BEGIN_ and END_ macros, just like with commands above. Any code between these two macros can use "var" to access the associated cvar. Example: BEGIN_CUSTOM_CVAR (splashfactor, "1.0", CVAR_SERVERINFO) { if (var.value <= 0.0f) var.Set (1.0f); else selfthrustscale = 1.0f / var.value; } END_CUSTOM_CVAR (splashfactor) This example also illustrates bounds checking for cvars. The basic idea is: if (variable is within range) else Calling Set() will automatically execute the code block again, so you must make sure that you do nothing but reset the value of the cvar if it's out of range. The code block will also be executed at program startup when the cvar is constructed. The DObject class hierarchy =========================== Most classes in ZDoom 1.18 are descendants of the DObject class. This class provides basic typechecking and serialization functionality. In a future version of ZDoom, it will also provide a means to interface C++ code with DoomScript. If you want to be able to perform type-checking on a class, save it to disk, or access it from DoomScript (in a later version), it should be a descendant of DObject. The basic features of DObject require that you use some of the macros in dobject.h: DECLARE_CLASS, IMPLEMENT_CLASS, DECLARE_SERIAL, and IMPLEMENT_SERIAL. For a class that won't be saved to disk, do this: class MyClass : public DObject { DECLARE_CLASS (MyClass, DObject) public: ... Rest of class definition ... } Then someplace else, where you define the class, add this line: IMPLEMENT_CLASS (MyClass, DObject) (You should be sure to replace MyClass with the actual name of your class and DObject with the name of your class's superclass.) For a class that can be saved to disk (ie, a serializable class), use DECLARE_SERIAL and IMPLEMENT_SERIAL instead. Serializable classes require default constructors. If you don't want anybody to be able to create an instance of the class using the default constructor, you can hide it inside the private portion of the class definition: ... private: MyClass () {} ... Serializable classes also require that you define a Serialize() method. This function should call the superclass's Serialize() method and then either store or extract the fields for the class. (Whenever you change the contents of a class, be sure to change its accompanying Serialize() method.) A generic Serialize() looks like this: void MyClass::Serialize (FArchive &arc) { Super::Serialize (arc); if (arc.IsStoring ()) { arc << m_field1 << m_field2 << m_field3 << ... ; } else { arc >> m_field1 >> m_field2 >> m_field3 >> ... ; } } There are also some structs that use some serialization code that you should be sure to change if you change the structs. The code for serializing sector_t and line_t can be found in P_SerializeWorld() in p_saveg.cpp. The code for serializing player_t is in p_user.cpp as player_t::Serialize(). To determine the type of an object, you can use the IsA() and IsKindOf() methods of DObject. IsA() tests if the object is an instance of a specific type of class, and is probably not what you want. IsKindOf() checks if an object is a specific type of class or one of its descendants. Example: Suppose anActor points to an instance of AActor. (anActor->IsA (RUNTIME_CLASS(DThinker))) will return false because AActor is not the same as DThinker, but (anActor->IsKindOf (RUNTIME_CLASS(DThinker))) will return true because AActor is a descendant of DThinker. Pointer cleanup =============== Whenever an object is destroyed, ZDoom will attempt to NULL any pointers that pointed to that object. This eliminates various anomalies caused by trying to reference stale pointers. This mechanism is not entirely transparent and requires some work on your part. When you create a subclass of DObject that contains pointers, you should use the IMPLEMENT_POINTY_SERIAL or IMPLEMENT_POINTY_CLASS macros instead of IMPLEMENT_SERIAL or IMPLEMENT_CLASS. Following the macro, you list each element of the class that points to a DObject (or a subclass of it). If the pointer points to something that is not a DObject (such as a char *), you do not need to include it in this list. After you list all the pointers, use the END_POINTERS macro. The implementation for the AActor class looks like this: IMPLEMENT_POINTY_SERIAL (AActor, DThinker) DECLARE_POINTER (target) DECLARE_POINTER (lastenemy) DECLARE_POINTER (tracer) DECLARE_POINTER (goal) END_POINTERS If you add pointers to any class, you need to update this list accordingly. The player class uses a hack, because it is not derived from DObject, to clear pointers. See player_t::FixPointers() in p_user.cpp if you add any pointers to player_t. When you want to destroy any object derived from DThinker (this includes all actors), call that object's Destroy() method. Do not use delete, because it is not guaranteed to perform all necessary cleanup. If the object is not derived from DThinker, however, you can safely call delete on it. As an example, if you have an instance of AActor called actor, and you want to get rid of it, use: actor->Destroy (); If you have an instance of DCanvas called canvas and want to get rid of it, either of the following will work (but not both): canvas->Destroy (); or delete canvas; Thinkers ======== Almost everything that can affect the appearance of the world is implemented as a subclass of DThinker. The important base classes are: DPolyAction A polyobject that is doing something (DRotatePoly, DMovePoly, or DPolyDoor). DSectorEffect An effect that occurs inside a certain sector. This is further broken up into DLighting (for lighting changes) and DMover (for floor/ ceiling movement). DMover is also subclassed into DMovingFloor (for moving floors only) and DMovingCeiling (for moving ceilings only). For effects that move both the floor and ceiling, subclass DMover and have a look at either DPillar or DElevator. AActor This is probably the most important. All players, monsters, items, ammo, weapons, and anything else that appears as a sprite in the world is an instance of AActor. This is nowhere near as encapsulated as I would like it to be because of the necessity to maintain compatibility with DeHackEd patches until DoomScript is done. This means that there is no easy way to subclass AActor and spawn an instance of the subclass in the map. I don't like it, but it's a step in the right direction. To find all instances of a specific type of thinker in a map, you can use the TThinkerIterator template class. It's usage is simple: TThinkerIterator iterator; TypeOfClass *object; while ( (object = iterator.Next ()) ) { // do something with the object } AActor methods ============== A few of the functions that operate on actors have been moved inside the AActor class: P_ClearTidHashes() -> AActor::ClearTIDHashes() P_AddMobjToHash() -> AActor::AddToHash() P_RemoveMobjFromHash() -> AActor::RemoveFromHash() P_FindMobjByTid() -> AActor::FindByTID() P_FindGoal() -> AActor::FindGoal() P_UnsetThingPosition() -> AActor::UnlinkFromWorld() P_SetThingPosition() -> AActor::LinkToWorld() AActor::SetOrigin() has also been introduced. It simply moves the actor to a specific location on the map without performing any collision detection. A bit more on serialization =========================== The serialization mechanism is a fairly standard, yet powerful, framework that can save a bunch of objects to disk and properly maintain pointers between them. When you store a pointer to an object, the object will also be stored to disk if it hasn't been already. When you read the pointer, the object it points to will also be read from disk if it hasn't been already. This allows pointers to objects to be preserved without any extra effort on your part. Pointers to something other than a DObject or one of its descendents need to be implemented specially by overloading operator << (see info.h for a few of these). If you're not sure how serialization works, take a look at some of the serialization functions for the existing classes by grepping through the source for "::Serialize" (without the quotes).