|
An Introduction to the Lithtech Engine
by Ashley Matheson
Welcome to the first in what I hope will be a series of mod tutorials/discussions
for Shogo (and hopefully this will be useful to you Blood 2 types as well).
Let me give you a brief background about myself first (after all, you should
know a little about the person writing all this stuff). My name is Ash Matheson
and I've been working with the Lithtech Engine since the Engine SDK was publicly
released in January (of 1998). I've come to appreciate all the hard work that
Mike, Brad, Jeremy and the rest of the guys have put into the engine. It is a
work of beauty. I've also been involved in writing some technical documentation
for the engine, so I've had a fair bit of exposure to the engine in all it's
glory. It truly is a sight to behold.
Anyway, enough rambling. Most of you folks out there are chomping at the bit,
getting ready to start developing your own mods. However, since the source
code hasn't been released yet, it's pretty hard to start writing about mods.
But I can start talking about what we are going to need to know in order to
prepare ourselves for developing mods. And that's the topic of this discussion.
I'm a programmer. I'm not a level editor, nor a graphic artist (I do however
do 3D, but that's a whole other story). So the focus of my tutorials will
always be programming (with maybe a slide or two into model editing and level
editing here and there).
So, what should you, as the budding developer, know about developing mods for
Lithtech? Well, first off, you are going to need some tools in front of you.
So far, I've only used Visual C++ 5.0 to compile programs with the SDK. However,
any other C++ compiler that will generate Win 32 DLL's should work. Remember,
that's a big SHOULD. I'll be getting said compilers (all the free ones, that
is ...) and trying them when the source code is made available.
Next, I would advise digging out any books you have on C++ and giving them a
read over. Sure you can create your own add-ons in C, but understand that
there is a lot of FREE code in the Shogo source. And the Monolith boys are
using C++ pretty extensively. So get up to speed on C++. It's more than worth it.
OK. As stated above, the goal of this document is not to create a mod, but to
help you understand what you are going to have to know about Lithtech in order
to create a mod. It'll be pretty short this time around, because I'm just going
to talk mostly theory. In the next couple of supplements (up until the release of the
source code) that's what I'm going to concentrate on. There will be snippets of
code, but since it's pretty certain that you won't have access to the development
tools, I'll try and keep the discussions simple.
From a developers standpoint, you are only going to be developing two dll's.
The CSHELL.DLL and OBJECT.LTO files. Let's talk about what each of them is individually.
The first thing to understand is that the Lithtech engine is Client/Server.
Right from the ground up. There is a Client portion of the program that handles
menus, displaying of the world, User Interface, Heads up display, initiating
Network communications, connecting to hosts ... anything that can be considered
local to the player. Think about it this way, the client will handle things
that no one else needs to be aware of. On the Server side, we handle the
physics of world objects, placement of object in the world, story line
advancement ... anything that has to do with the world and the players in the
world (that everyone can see).
That's the brief overview of Client/Server. From the filenames, you may be
able to get an idea as to what DLL goes where. However, to make everything crystal clear ...
CSHELL.DLL is the Client SHELL DLL. In this DLL, you define the behavior of
how the client behaves. You determine what the Display looks like, what menus
get displayed, what (and how) the first person weapons look like, as well as
network connection and score display. Additionally, the Client Shell (What
I'll be referring to from now on instead of CSHELL.DLL. It's easier to
understand that way) does our world rendering for us. The server doesn't do
any rendering at all (most of you already knew that, but it's best to say it
for those of you that don't know).
OBJECT.LTO is a part of the Server. It, in part, defines the behavior of how
the server acts, but additionally, defines all the object that you will have
available in your world. All your Bad-guys, Non-Player Characters, weapons,
ammo, Health, Power-ups ... you get the idea .. are all defined in this DLL.
Additionally, OBJECT.LTO is used by the world editor for placement of objects
in the game. DEdit (the world editor) will read your OBJECT.LTO DLL and find
all the objects that have published properties and allow you to place then in
your level as well as set their attributes. I'm going to keep this discussion
pretty high level, but in the next tutorial, I certainly will be looking at how
you go about creating a very simple world object you can use in your level.
Additionally, if you've looked through the files in your Shogo directory,
you'll have undoubtedly found the following:
Shogo.exe
ShogoSrv.exe
Client.exe
I think everyone by know (if you've at least played the game) knows what
SHOGO.EXE is. It's the launcher program. It brings up the following window:
That and some Network connection stuff is all this front end does. It has
very little to do with actual game play. The game is actually run by a call
to CLIENT.EXE. That is, for all intents and purposes, the Lithtech Engine (as
well as all the DLL's that go with it).
OK. Before you go looking for those two files to start mucking with them, you're
out of luck. You won't find them. Both the OBJECT.LTO and CSHELL.DLL
files are compressed and stored inSHOGO.REZ (as well as in SOGOP.REZ)
So that leads us into another set of questions. When are we going to be able to
have access to the tools that will allow us to create worlds, models and customize
the way the game is played? According to Matt Saettler, Director of 3rd Party
Development :
".., the mod tools will be out shortly after the Patch is released.
The patch is due Nov. 15."
Now let's start talking about putting in some code that will actually do
something. Let's start off on the Server side of things. If you remember, I
said that OBJECT.LTO defines how the server acts as well as defining world
object, their properties and behaviors. There's a couple of other very important
things that it does, and we will talk about them shortly.
Most everything in Lithtech is C++ oriented. Personally, I think that's a good
thing. That being the case, inheritance plays a huge part in Lithtech. You
define your sever implementation as a derived class of a base Lithtech Server
class. OK. That may sound like a lot, but it's not. If you're familiar with
OOP, this next section may bore you a bit, but I'm hoping it's a good read anyway.
Lithtech defines a base class CServerShellDE. This base class manages all the
low level server functionality and via virtual functions allows you to extend
what the server can do. Here's a bit of what CServerShellDE looks like:
// Notification when new clients come in.
// You must create an object to represent the client.
// It uses the object's position to determine what the client can see.
virtual void OnAddClient(HCLIENT hClient) {}
virtual void OnRemoveClient(HCLIENT hClient) {}
virtual LPBASECLASS OnClientEnterWorld(HCLIENT hClient) {return NULL;}
virtual void OnClientExitWorld(HCLIENT hClient) {}
// Called before and after you switch worlds.
virtual void PreStartWorld(DBOOL bSwitchingWorlds) {}
virtual void PostStartWorld() {}
// Incoming message notification.
virtual void OnMessage (HCLIENT hSender,
DBYTE messageID,
HMESSAGEREAD hMessage) {}
virtual void OnObjectMessage (LPBASECLASS pSender,
DDWORD messageID,
HMESSAGEREAD hMessage) {}
// Command notification.
virtual void OnCommandOn(HCLIENT hClient, int command) {}
virtual void OnCommandOff(HCLIENT hClient, int command) {}
// Update loop callback.. do whatever you like in here.
// Time since the last Update() call is passed in.
virtual void Update(DFLOAT timeElapsed) {}
That's a lot of virtual functions. And as you can see, most of them don't do
a lot. So what are we looking at here? Let's quickly review each virtual
function and see what it does.
OnAddClient
OnRemoveClient
These two virtual methods are run whenever the Lithtech server finds that a
client has attached themselves to the server (OnAddClient) or the client has
disconnected from the server (OnRemoveClient). Understand that these two
functions are completely different than the next two virtual methods we are
going to see.
OnClientEnterWorld
OnClientExitWorld
These two virtual methods are run whenever the Lithtech server detects that a
client has entered the world or has left the world. There's a big difference
between these methods and the methods defined above. There are two entry points
into any world that a Lithtech Engine is serving. First, when the person tries
to connect to the server and make sure they can get a reliable connection (the
OnAddClient method). Then, when the client has loaded the world and is ready
to play, the OnClientEnterWorld is called. The reverse stands true. When the
Client decides to leave a game, and they exit the level, the OnClientExitWorld
method is called, then the OnRemoveClient method is called.
Why do this? Well, imagine that you are trying to connect to a server that is
full. The OnAddClient on the server can have an internal table with a list of
the names, IP's, scores, ... whatever of currently on-line players. If the
server is full, it can then tell the client that it cannot join the game. Also,
it can send back a list of all the users that are on this server. I think you
get the idea.
PreStartWorld
PostStartWorld
Another twin set of functions. The first, PreStartWorld is called before the
Server starts to load a new (or first) world into the engine. This is a good
point to send out a message to all the clients currently attached with a message
indicating a level change, estimated time to load, ... you get the idea.
The second of the twin functions, PostStartWorld, is called after the Server
has finished loading the world. Again, this could be a good opportunity to send
a "Level beginning now' message to all the clients. Or to inform all the clients
that the server is ready to handle the game. Also, if your game has any time
limits/frag limits, now is a good time to set them.
OnMessage
OnObjectMessage
One big thing in Lithtech is Messages. If you want objects to communicate to
each other, you need to send a message from one to another. The same thing goes
for the server talking to the Clients that are attached to it (and vice versa).
If, for example, you have a client that wants to send a message to everyone (the
famous 'say' commmand), you first need to send a message to the server from the
client saying "Hey, I have a message!", as well as the string that is the message.
The server then broadcasts that string to all the attached clients with another message.
OK, now that you get an idea about messages, what does OnMessage and OnObjectMessage
do? OnMessage handles generic messages from Clients. OnObjectMessage handles
messages to the server from Objects in the world (Doors, crates, that exploding
wall, ... you get the picture).
Now there are a set of parameters that go along with this. OnMessage's parameter
list looks like this:
HCLIENT hSender, DBYTE messageID, HMESSAGEREAD hMessage
hSender is the Client (which is of type HCLIENT, a Handle to a Lithtech Client),
messageID is a unique Identifier for the message received, and hMessage, a
Handle to a Message. With the Handle to the Message, we can read in any further
information. For example, in the 'say' example, we would have a message ID of,
say 12, indicating a 'say' message has been received from the client hSender.
We then use hMessage to get the string that was sent. We will see how this can
be done in a future installment.
OnCommandOn
OnCommandOff
If you are used to windows programming, you may have seen something very similar
to this. In Windows programming, when you press a key , you get a WM_KEYDOWN
message. When you release the key that was depressed, you get a WM_KEYUP message.
Well, these two virtual methods work in the same vein.
In Lithtech, you bind Actions to Keys, Mouse buttons, Joystick Buttons, ...
whatever. That's what the autoexec.cfg file is all about. What we have here
is a process when an action key is either depressed or released, you will get
a notification of it. OnCommandOn works like the WM_KEYDOWN, and OnCommandOff
works like WM_KEYUP. Again, there are arguments that go along with these functions:
HCLIENT hClient, int command
where once again, we see a hClient variable of type HCLIENT (the client that
pressed the key) and an int value of the command that was activated.
Update
Finally (for now) we see the Update method. This is where the game updates
itself every 'frame'. This is where you can process user movement (from the server),
update objects in the world (respawn weapons), or whatever else you want to do
in the world. Also, it's a good place to update any level timers you may have.
So, that, in a nutshell, is what a Lithtech CServerShellDE base class looks
like. From this, we will create our server. But not quite yet. There's another
thing that we need to do that is directly related to any server. We need some
way to represent our player (especially in a multi-player game) in the world.
Let's look at that, and another base class, next.
We are going to need an object in the world that will represent 'us'. This
could be a model, a camera, a light, whatever we want it to be. But somehow
we need a way to depict it in our world. So what we are going to do is create
a Player class that represents the player in the world. But to do this, we
also need to look at the next base class in our list, the aptly name base class BaseClass.
BaseClass only has two virtual functions that you can override. They are:
EngineMessageFn(DDWORD messageID, void *pData, DFLOAT lData);
ObjectMessageFn(HOBJECT hSender, DDWORD messageID, HMESSAGEREAD hRead);
Once again, this should look very familiar. We've got similar methods defined
for the server that handle when the BaseClass object receives a message from
the server, and when the BaseClass object receives a message from another object
(of class HOBJECT).
So what have we got so far? Two base classes. One that define our server and
another that defines an object in the world. Believe it or not, that's a good
part of what we are going to need to create our games.
Let's now look at what we need to do to get a server alive. Remember, this is
not the code for Shogo. This is simply how the Lithtech engine works. From
this we will eventually see how the Shogo server works (when they release the
source code to the public).
Let's see the definition of the Server Shell object that we are going to create.
For demonstration purposes, we're going to call this server CSampleServerShell :
#ifndef __SAMPLE_SERVERSHELL_H__
#define __SAMPLE_SERVERSHELL_H__
class CSampleServerShell : public CServerShellDE
{
public:
virtual LPBASECLASS OnClientEnterWorld(HCLIENT hClient);
virtual void OnClientExitWorld(HCLIENT hClient);
virtual void OnMessage(HCLIENT hSender,
DBYTE messageID,
HMESSAGEREAD hRead);
};
#endif // __SAMPLE_SERVERSHELL_H__
From this, you can see that we have created a new class (this code snippet is
from a file called sample_servershell.h) derived from the base class CServerShellDE.
In this derived class, we override the methods OnClientEnterWorld, OnClientExitWorld and OnMessage.
And that's it. Well, that's it for the definition of the new server shell.
Now we need to look at Implementing these methods. We switch over the file
sample_servershell.cpp and take a look-see:
// --------------------------------------------------------------- //
// CSampleServerShell member functions.
// --------------------------------------------------------------- //
LPBASECLASS CSampleServerShell::OnClientEnterWorld(HCLIENT hClient)
{
ObjectCreateStruct createStruct;
LPBASECLASS pObject;
// Create an object to represent the client.
INIT_OBJECTCREATESTRUCT(createStruct);
pObject = GetServerDE()->CreateObject(
GetServerDE()->GetClass("Player"),
&createStruct;);
((Player*)pObject)->m_hClient = hClient;
// Store the object pointer with the client for future reference.
GetServerDE()->SetClientUserData(hClient, pObject);
return pObject;
}
OK. Here in the OnClientEnterWorld we see that a new client has attached to
the server. What we are going to do here is create a new 'Player' object when
the client attaches to the server. To do that, we need some new structures.
First off, we have a structure called ObjectCreateStruct. Gee, with a name
like that, I wonder if it is a Struct that tells us how to Create and Object?
You betcha! Here's what the ObjectCreateStruct actually looks like:
unsigned short m_ObjectType;
DDWORD m_Flags;
DVector m_Pos;
DVector m_Scale;
DRotation m_Rotation;
D_WORD m_ContainerCode; // Container code if it's a container.
// It's in here because you can only
// set it at creation time.
DDWORD m_UserData; // User data
char m_Filename[MAX_CS_FILENAME_LEN+1]; // This is the model, sound, or
// sprite filename.It also can be
// the WorldModel name. This can
// be zero-length when it's not
// needed.
char m_SkinName[MAX_CS_FILENAME_LEN+1]; // This can be zero-length.. if
// you set it for an OT_MODEL,
// it's the skin filename.
// Server only info.
char m_Name[MAX_CS_FILENAME_LEN+1]; // This object's name.
float m_NextUpdate; // This will be the object's
// starting NextUpdate.
float m_fDeactivationTime; // Amount of time before object
// deactivates self
There's a lot of things that we can put into this structure. I won't go into
detail here, but I will in a future installment. However, one thing that you
will see is a macro called OBJECTCREATESTRUCT. This was called on the variable
createStruc (remember, it's of type ObjectCreateStruct. Also, we have a variable
pObject that is of type LPBASECLASS. LPBASECLASS is a generic object that can
hold any world object (Polymorphism, anyone ...?).
Next, we have an example of one of the most important functions available in
Lithtech. GetServerDE() is THE server command. It returns a pointer to the
server, and allows us to access all the neato-wiz-bang functionality of the server.
Waitaminute, I hear you asking. We've already got access to the server through
the virtual functions, right?
Well, kinda-sorta. The server we implemented is the server SHELL. It's what
the server expects us to handle. There are functions the server has available
to you, the application programmer, that make your life less of a living hell.
And you access these functions through a call to GetServerDE(). All it does is
return a pointer to the Server object, thus allowing us access to those methods
that Lithtech has made available. And one of those function is the CreateObject
method. CreateObject allows us to create a new object on the server, whenever
we make the call to this object. Here's the line once again (just to jog your memory);
pObject = GetServerDE()-<gtCreateObject;(GetServerDE()-<gtGetClass;("Player"), &createStruct;);
So, the server function CreateObject (accessed through GetServerDE()-<gt;) creates
a new object of type "Player", through yet another server function call of GetClass.
Also, it passes in the createStruct that we defined earlier. This is used to
create a new player object. We will look at the player object very soon.
Now what do we do when we decide to leave the world?
void CSampleServerShell::OnClientExitWorld(HCLIENT hClient)
{
LPBASECLASS pObject;
// Get rid of their object.
pObject = (LPBASECLASS)GetServerDE()->GetClientUserData(hClient);
if(pObject)
{
GetServerDE()->RemoveObject(pObject->m_hObject);
GetServerDE()->SetClientUserData(hClient, DNULL);
}
}
This is pretty simple. First off, we make sure that the client that just left
was valid. If it was, we remove the object with a call to RemoveObject, and
then set that client to Nothing. However, instead of using a NULL to do that,
we use a DNULL. Why DNULL? Remember that at some point in the near future,
games made with the Lithtech engine will be portable to other systems. And it's
more than possible that certain constants will be defined differently. So, for
consistency sake ...
Finally, we need to handle any messages coming from the server. We want the
server to handle any rotation messages for the object. There's a reason we are
going to do it this way.
We want the client to handle rotation of the user's viewpoint immediately.
So we process all turning commands on the client as fast as is possible. We
then send to the server a message ID of 1 to indicate that the user has turned
in a new direction. We pass all that information along and the server then
set's the player's object on the server to match that rotation. This way, you
can immediately rotation on the client side, and if there is any lag present in
the system, that should be OK. Movement will be handled on the server, and we
will see that in the Player class.
void CSampleServerShell::OnMessage(HCLIENT hSender, DBYTE messageID,
HMESSAGEREAD hRead)
{
LPBASECLASS pObject;
DRotation rotation;
// Is it a rotation message?
if(messageID == 1)
{
// Ok, get the client's object.
pObject = (LPBASECLASS)GetServerDE()->GetClientUserData(hSender);
if(pObject)
{
// Set the object's rotation.
GetServerDE()->ReadFromMessageRotation(hRead, &rotation;);
GetServerDE()->SetObjectRotation(pObject->m_hObject, &rotation;);
}
}
}
OK. A quick discussion about what just went on. We talked about WHY we did
this, now we need to talk about HOW we did this. The Server will get from the
client a message ID of 1, which indicates that a rotation has occurred.
Additionally with that message there will be rotation information passed along.
To get that information, we need yet another server function called ReadFromMessageRotation.
This reads in the rotation values sent to the server (represented by hRead) and
is stored into the variable rotation (which is why we have the '&&' in front of
the rotation variable.
Next, we want to set the Player object's rotation, on the server, to match what
was sent from the client. This is done with the SetObjectRotation method (which
again is another handy-dandy server function).
And that's a good part of the server. Now we have one last thing to look at,
and that is the Player object. Or at least what the server is going to represent
as our player. Let's look at the Player header file (Player.h)
class Player : public BaseClass
{
public:
Player();
DDWORD EngineMessageFn(DDWORD messageID, void *pData, DFLOAT lData);
// The client associated with this player.
HCLIENT m_hClient;
};
As you can see, the player is derived from the BaseClass class. Since BaseClass
has an overrideable EngineMessageFn method, we override it (you will see why very shortly).
And, other than the typical constructor, we have a variable that represents a
pointer to the client. This gets set when the client enters the world, and
you never know, we may have need of it, to directly access this player's client.
It's a good idea to have, in any case.
Let's see the implementation of this Player class.
Player::Player() : BaseClass(OT_CAMERA)
{
}
OK. This is a constructor for the Player Class. The only thing to notice here
is that we create the Player object and call the constructor for the BaseClass
ancestor. Note that we pass in the Constant OT_CAMERA. This tells BaseClass
that we are creating a camera object that will represent our player. There are
a LOT of different types of objects we can create. But for now, let's leave it
as being a camera.
Next, we have the EngineMessageFn to deal with. This method gets passed
messages from the engine (No surprise there, that's why it's named that way).
But what kind of messages can we expect the engine to send this class. For now,
we're only going to concern ourselves with three of them: MID_PRECREATE,
MID_INITIALUPDATE and MID_UPDATE.
Let's discuss them briefly.
MID_PRECREATE
This message is sent to the object just before the engine creates the object.
So here you can set any properties that are relevant to the object.
MID_INITIALUPDATE
This message is sent to the object after the object has been created and just
before it is initially updated. To update the object, you must call the Server
function SetNextUpdate, which tells the engine when it should update the object next.
MID_UPDATE
This message is sent to the object whenever it needs to be updated. This is
where we are going to process any user movement commands.
Now that we've talked theory, it's time to look at it in practice.
DDWORD Player::EngineMessageFn(DDWORD messageID, void *pData, DFLOAT lData)
{
float moveSpeed = 120.0f;
float terminalVelocity = 400.0f;
DVector velocity, forward, right, up, temp;
DRotation rotation;
ObjectCreateStruct *pObjectCreateStruct;
DVector dims;
// See serverobj_de.h (near line 100) for descriptions of the messages.
switch(messageID)
{
case MID_PRECREATE:
{
pObjectCreateStruct = (ObjectCreateStruct*)pData;
pObjectCreateStruct->m_Flags |= FLAG_SOLID;
break;
}
case MID_INITIALUPDATE:
{
// Tell LithTech to update us as soon as possible.
GetServerDE()->SetNextUpdate(m_hObject, 0.001f);
// Give us some dimensions.
VEC_SET(dims, 14, 50, 25);
GetServerDE()->SetObjectDims(m_hObject, &dims;);
break;
}
case MID_UPDATE:
{
// Check what commands are on and apply movement.
GetServerDE()->GetVelocity(m_hObject, &velocity;);
GetServerDE()->GetObjectRotation(m_hObject, &rotation;);
GetServerDE()->GetRotationVectors(&rotation;, &up;, &right;, &forward;);
if(GetServerDE()->IsCommandOn(m_hClient, COMMAND_FORWARD))
{
VEC_MULSCALAR(temp, forward, moveSpeed);
VEC_ADD(velocity, velocity, temp);
}
if(GetServerDE()->IsCommandOn(m_hClient, COMMAND_BACKWARD))
{
VEC_MULSCALAR(temp, forward, -moveSpeed);
VEC_ADD(velocity, velocity, temp);
}
// Slow down for when they aren't pressing keys.
VEC_MULSCALAR(velocity, velocity, 0.8f);
if(VEC_MAG(velocity) < 0.05f)
VEC_SET(velocity, 0, 0, 0);
// Update the object's velocity.
GetServerDE()->SetVelocity(m_hObject, &velocity;);
// Register for another update..
GetServerDE()->SetNextUpdate(m_hObject, 0.001f);
break;
}
}
// Pass the message on down.
return BaseClass::EngineMessageFn(messageID, pData, lData);
}
Oops, one or two more things to talk about here. At the very top of this method,
you see variables that are being created from the types DVector and DRotation.
Once again, these are new structures defined in Lithtech. A DVector is a vector,
short and sweet. I'm not going to talk about what a vector is. If you don't
know what a vector is, and you can't find any information on the subject, e-mail
me and I'll help you out. DRotation is a little more involved. Instead of
storing the rotation as a set of degrees, it's stored as a Euler angle. What's
a Euler angle? Well, it's an awful lot like a vector, but instead of storing a
direction, each vector component stores a rotation angle around each of the
three major axis (X, Y and Z).
That's a quick little math lesson. If there's a call for it, in a later
installment, I'll discuss the 3D math being used in Lithtech.
Let's look at each message, starting with MID_PRECREATE. This message handler
does nothing more than set the object's flag to be solid. If an object's solid,
you can't pass through walls, floors, other objects. In essence, it makes you
an object that can collide with other things.
In MID_INITIALUPDATE, we set the next update of this object to be very soon
(0.001 of a second, to be precise). This way we guarantee that we get a
MID_UPDATE message. But I'm getting ahead of myself. The very last thing that
we do is set the dimensions for this object. As it stands, our player, or the
player's camera, has a small bounding box used for collisions. We need to make
it a little bigger (to help out with clipping problems). We set a DVector
variable to define the width, length and height of a box to be used as a 'collision' box.
Finally, we have the MID_UPDATE message. This is where we can move out player
around. First, we get the object's current Velocity and rotation with calls to
GetVelocity and GetObjectRotation (yet more handy Server functions ...).
Now that we have these, we can use them to move our player (if we need to).
We check to see if the user has the action keys bound to the Actions "COMMAND_FORWARD" or "COMMAND_BACKWARD"
depressed with the call to IsCommandOn. If either of these commands are active,
we move the character along or back along the forward vector (depending upon
which key is being pressed).
Next, we slow down the player's movement by 20% (to give the illusion of
friction through the air). We then check to see if the player has slowed down
enough to be considered a 'stop' ( to avoid really small values of forward or
backward velocities causing a 'drift' effect). Otherwise we continue on and
add the new velocity to the current velocity. Finally, we set the next update
to a very small value (read that as being ASAP).
OK. I think that's enough for one installment. It's a brief introduction
into how the Lithtech Engine works and what it takes to create a server. In
the next installment, I'll talk more in-depth about the OBJECT.LTO file as well
as the CSHELL.DLL (which we only briefly discussed). Remember, these are the
keys to developing MODS for Shogo.
Until then ...
Ash (Madman99) Matheson
madman99@nbnet.nb.ca
|