So, you want to customise half-life to suit your own needs? Well, you can start by downloading the source code. Go to Wavelength and get it at once! |
Finding your way around
Now that you've read my other two tutorials, and probably browsed around
the source a bit I expect your mighty confused! The Half-Life source
code initially looks a lot more complicated than it is, this is because
in C++ you have many objects but no idea where these objects are
createrd or what they arre used for. You can start off by opening the
original gmae dll source code (not the stuff from the last tutorial) in
Visual C++.
Where it all begins
First of all open the 'h_export.cpp'. This is a short file, but is, in
essence, where the whole DLL begins. The function DllMain is a standard
entry point for DLL's (much like the main function or WinMain function).
You will
notice that DllMain doesn't actually do anything, why you may ask? How
does the game know what to do with it all if the entry point does
nothing. Well, now open the file mp.def, you will se that it includes
these lines:
EXPORTS GiveFnptrsToDll @1This tells the compiler that a function called GiveFnptrsToDll needs to be exported, if you go back to 'h_export.cpp' you will notice the implementation of this function. This function is called by the engine to tell the DLL about certain information. There are two key blocks, the engine functions and the global variables. Def files are special files used when writing your own DLLs which allow you to export special information and functions.
But, of course, it has to be more compilcated than that, there have to be times when the engine tells the dll to do something. If you open up 'c_base.h' and scroll to line 56 you will see this code:
extern "C" EXPORT int GetEntityAPI( DLL_FUNCTIONS *pFunctionTable, int interfaceVersion );The use of the EXPORT keyword here indicates that this function is also exported from the DLL. You will find the implementation on line 75 of 'c_base.cpp'. So what does this function do> Well, if you look at the implementation you will see that it copys the global dll variable gFunctionTable intop the parameter pFunctionTable. Why? Well, gFunctionTable contains a load of functions the DLL needs to tell the engine about, it includes pointers to these functions so that the engine can call them at the appropriate times. These functions are defined in client.cpp and are listed below:
GameDLLInit, // Called to initalise the DLL // I'm not sure what these do (yet) DispatchSpawn, DispatchThink, DispatchUse, DispatchTouch, DispatchBlocked, DispatchKeyValue, DispatchSave, DispatchRestore, DispatchObjectCollsionBox, // Called to Save and Restore information (used in saveing games) SaveWriteFields, SaveReadFields, // Used to save, restore and reset the level data (I think this is used between levels to backup // the old data and clear the new, when ou return to a level it is restored) SaveGlobalState, RestoreGlobalState, ResetGlobalState, ClientConnect, // A client connects (you can reject them here) ClientDisconnect, // A client disconnected (you clean up afteer them here) ClientKill, // A client types the 'kill' command at the console (I think) ClientPutInServer, // Called every time a client spawns or respawns ClientCommand, // Called whenever a client executes a command (you can add your own commands with this) ClientUserInfoChanged, // Called whenever a client changes their custom data (such as their name or model) ServerActivate, // Starts a server (?) // Called at the start of the player think cycle (does most of the work) (before StartFrame) PlayerPreThink, // Called at the end of the player think cycle (after StartFrame) PlayerPostThink, // Starts a new frame, does things such as tells each object to run it's 'think' function StartFrame, // I think this is called when a new level is started ParmsNewLevel, // If that's the case then this is when the level is changed ParmsChangeLevel, GetGameDescription, // Returns string describing current .dll game. PlayerCustomization, // Notifies .dll of new customization for player. SpectatorConnect, // Called when spectator joins server SpectatorDisconnect, // Called when spectator leaves the server SpectatorThink, // Called when spectator sends a command packet (usercmd_t)So now you'vwe seen where the DLL and the Engine overlap. You should be able to see that the engine gives the DLL access to a number of it's functions and some of it's global variables and that the DLL gives the engine access to some of the DLL's functions. However, the class based system masks many of the above functions through the use of classes. In important class is CGameRules, CGameRules is the base class for game types (it is not used itself). In the standard game code you will see 3 game types, CHalfLifeMultiplay, CHalfLifeRules and CHalfLifeTeamplay which handle multi-player games, single player games and teamplay games repsectivley.
WARNING : If you are not using MS VC++ some of this may not make sense to you, basically I am describing the class listing provided by VC++, if your program has similar functionality then that's where to look. If not then finding your way around the source may be tough, but look at the file names and attemp to match them to the classes I describe.
So let's have a look what the CGameRules derivates actually do. If you expand ClassView under the 'mp classes' item and then expans the CHalfLifeMultiplay class you will see a number of functions, double click a few of them to see what their definitions look like. There are various functions for adding points to players, ammo, determining if clients should be allowed to connect etc. etc. You should be able to see that customization of these classes allows you to modify the way the game works on a glboal scale. When you want to make a change which affects every player or the whole world, such as increasing the score that gets added from 1 to 2, this is where you'd make it. You can even customize InstallGameRules to create your own custom derivative of CGameRules - allowing you to add more than one set of rules to your mod.
Objects and things
So far I've show you the entry points to the DLL, and the main classes
for handleing a Half-Life game, what I haven't done is explaine how all
those objects get into the game and how the game knows what they do. In
this section I'm
going to introduce the way Half-Life handles objects in the game DLL.
Let's start by looking at an object, The Egon. Find the CEgon class in
class view and expand it. You will see a list of member functions and
variables, some of them such as 'PrimaryAttack' have clear uses, but
this still doesn't explain how
they get used. Now open up the fiule 'egon.cpp', we are going to take a
look at how the Egon is implemented as an item.
At the top of the file you will first see a number of #includes which
give egon.cpp access to global variables and class definitions. If you
were to add your own object you may well just copy this list of includes
to allow your file to access
similar global variables and objects. You will then see a number of
#defines which make changing constant values for the Egon quick and
simple, it is always a good idea to use defines for constant values
rather then just the values themselves
because this makes it easier to change EVERY instance of the value at
once. You will then see an Enum called egon_e - this defines the
animations for the egon, although the names are unimportant the order
and values are since they
are designed to match up with the order and values contianed in the
model file for the egon (a model file defines the 'geometry' of an
object, what it looks like, how it animates etc. etc.).
Then comes the class definition for CEgon, you will notice that it is
derived from the CBasePlayerWeapon class. This is where you get your
first glimpse of how the game works. From what you have seen in the
derivates of CGameRules you
should be able to see that weapons are probably handled in a similar way
- with basic functions defined in the 'CBasePlayerWeapon' class but
each weapon overriding the default behaviour to handle how they perform
the actions. This can
be seen with the 'PrimaryAttack' and 'SecondaryAttack' (CEgon does not
have a secondary fire, but in the future one of my tutorials will show
you how to add one) functions which are called from the
CBasePlayerWeapon pointer to handle
primary and secondary fires for the weapons. In fact the entire object
heirarchy is derived from a single object, CBaseEntity:
Object Heirarchy This shows only a small part of the entire object heirarchy CBaseEntity CBaseDelay CBaseToggle CBaseItem CBaseMonster CBaseCycler CBasePlayer CBaseGroup CBaseAnimating CBasePlayerItem CBasePlayerWeapon CEgonAs you can see, CEgon is a long way down the heirarchy, but still derived from CBaseEntity - meaning it has access to all the members and functions of an Entity. Now, as we contine through the file we come to the most important line is any object definition:
LINK_ENTITY_TO_CLASS( weapon_egon, CEgon );The LINK_ENTITY_TO_CLASS macro tells the engine that whenever a map file defines an object called 'weapon_egon' that it should use the class 'CEgon' to handle all of that objects functions (we will look at adding items to the map editor in a future tutorial, meanwhile one may be found on Wavelength) . Because CEgon is derived from CBasePlayerWeapon the game knows that it is a player weapon and because CBasePlayerWeapon is derived from CBasePlayerItem it knows that it is a player item and may be carried.
The final piece of information I want to describe is 'Think' functions.
'Think' functions always have a definition of void ClassName::ThinkName(
void ) and are called each 'frame' to handle whatever 'thinking' an
object needs to do.
There are two critical values related the think functions, the 'next
think time' and the 'current think function'. The following example
illustrates how a Snark sets it's 'HuntThink' function.
// In CSqueakGrenade::Spawn( ) // This line sets the think function to the member function 'HuntThink' SetThink( HuntThink ); // This line sets the next think time to 0.1 seconds from the current time (that's one frame from now, since one frame for the game DLL is 0.1 seconds) pev->nextthink = gpGlobals->time + 0.1; // In CSqueakGrenade::HuntThink() // Every time the think function is called the next think is reset, in order for our think function to be called again we need to set nextthink again pev->nextthink = gpGlobals->time + 0.1;Of course you can set nextthink to any time you want, for example you could have a grenade blow up in three seconds by setting the think to a custom function such as 'BlowUp' and the next think time to 'gpGlobals->time + 3.0'. Any function may be used for thinking as long as it has a void return type and takes no parameters.
Well, that's it for this tutorial so far, of course as I delve further into the DLL I'll add more information here, but I hope I've given you enough information to be able to find your way around the DLL now. I'd suggest double clicking a few classes, looking at what they are derived from and what functions they share in common. Try to get a 'feel' for where certain actions are handled in the game, look for where death messages are presented and search for function names to see where they are called and used. You may want to get familiar with the CBasePlayer class which handles all the players actions - take a good look at the PreThink function because this is where much of the work is handled.
The Client DLL
You thought that was it didn't you! Well, this is the newest addition to
this page, it describes how the client dll works. You can start by
opening up the client dll workspace.
If your using MS Visual C++ the first thing you will notice is that it contains far fewer classes than the game DLL and that most of these classes are related to the HUD. This is because the client DLL is responsible for handling all of the HUD graphics (communication between the Client and the Server will be handled in a future tutorial). I am not really going to look at the entry points here, if you are interested then you can search for similar function names to those in the game DLL or look at files which are not related to a particular class.
The two main classes are the CHud class and the CHudBase class, I'll deal with each in turn.
The CHud class is responsible for drawing each of the HUD elements if it
wants to be drawn and generally handling the game HUD and global
functions. If you did look up those entry points you would probably find
that the global variable
gHud (a CHud) is referenced.
However, the whole HUD is not incorperated into the CHud, instead each
HUD element is given it's own class and this class is added to a linked
list of elements in CHud to be drawn. This is where the CHudBase class
makes an enterance,
since the linked list is actually a linked list of CHudBase elements,
but each CHudBase element points to a derived class such as
CHudScoreboard - another example of pointers to a base class being used
to access their children. If you want
to add your own HUD element then the simplest thing to do is look at one
of the most basic elements such as CHudTrain (this displays the train
controls when you are driving a train), searching for CHudTrain brings
up the following list:
Searching for 'CHudTrain'... E:\Adam's Data Files\StandardSDKSource\cl_dll\hud.h(175): class CHudTrain: public CHudBase E:\Adam's Data Files\StandardSDKSource\cl_dll\hud.h(566): CHudTrain m_Train; E:\Adam's Data Files\StandardSDKSource\cl_dll\train.cpp(30): int CHudTrain::Init(void) E:\Adam's Data Files\StandardSDKSource\cl_dll\train.cpp(41): int CHudTrain::VidInit(void) E:\Adam's Data Files\StandardSDKSource\cl_dll\train.cpp(48): int CHudTrain::Draw(float fTime) E:\Adam's Data Files\StandardSDKSource\cl_dll\train.cpp(72): int CHudTrain::MsgFunc_Train(const char *pszName, int iSize, void *pbuf) 6 occurrence(s) have been found.You will notice that only two of these items are outside the train.cpp definition file for the CHudTrain class, and that one of these is the declaration of the CHudTrain class (class CHudTrain : public CHudBase). The other is a member variable of the CHud class, called m_Train. With this info it is a good idea to see where m_Train is used, so we do a search for m_Train:
Searching for 'm_Train'... E:\Adam's Data Files\StandardSDKSource\cl_dll\hud.cpp(100): m_Train.Init(); E:\Adam's Data Files\StandardSDKSource\cl_dll\hud.cpp(230): m_Train.VidInit(); E:\Adam's Data Files\StandardSDKSource\cl_dll\hud.h(566): CHudTrain m_Train; E:\Adam's Data Files\StandardSDKSource\cl_dll\train.cpp(27):DECLARE_MESSAGE(m_Train, Train ) 4 occurrence(s) have been found.Looking at this we see that the absolute minimum number of things we have to do to add a class is three (plus defining and implementing our new class) since DECLARE_MESSAGE is specific to the CHudTrain class (it is related to how the client and server communicate and will be dealt with is a future tutorial):