qzdoom/docs/classes.txt

304 lines
11 KiB
Plaintext

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)
<do something with it>
else
<set it to something that is in range>
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_s::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_s::FixPointers() in p_user.cpp if you add any
pointers to player_s.
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<TypeOfClass> 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).