Game Development Community

dev|Pro Game Development Curriculum

Variable Player Aspects

by Daniel Neilsen · 05/13/2002 (10:53 am) · 15 comments

Download Code File

Making variable player information


by Daniel "Wizard_TPG" Neilsen
wizardsworld@bigpond.com

Before we go too much further I should say this
DISCLAIMER - I am certainly not the best at C++ in the world but
this worked for me so it should work for you too.


This is a system I developed for my game Trakers: Bid for Glory, to
enable every player in the game to act realisticly. In most games,
all the players act the same. The are the same speed, can carry the
same amount, etc, etc. I wanted every player to be completely different
depending on that players variety of attributes, skills and weight of
equipment.

To this end I developed this system which enables you to change player
abilitys, such as speed, energy usage, jump abilitys, etc, etc dynamically
through the use of console commands, used by the script engine.

In this tutorial we will go through adding a single player specific variable to
the engine, in this case, player speed. If you follow the procedure you
can easily add more.

What we are going to do is not one of the simplist things to attempt and,
if you dont understand the basics of TGE networking, can be difficult to
get working. If you understand TGE networking please skip the next few paragraphs,
if not, read on.



TGE NETWORKING - THE BASICS (in the extreme)

For every player in a game, there is really two players. One that exists on
the clients computer and another that exists on the server, this one being
referred to as the "ghost". Information is updated between the ghost and
the player in the bitstream using the packData and unpackData functions.

It is important to realise whether you are altering data on the player or the
ghost when coding player variable information into the engine or it will
simply not work (or worse). It can be difficult to get your head around this
at times :/



STEP 1 - CONSOLE FUNCTIONS

We are going to add in a simple console function to alter the players
maximum speed settings and, also functions to return the current settings.
We also need to add in properties to the player that we can set to the
appropriate values.

Open player.h
Add the following lines to line 365. These lines are our speed settings
for the player:
S32 mcMaxForwardSpeed;
S32 mcMaxBackwardSpeed;
S32 mcMaxSideSpeed;
S32 mcMaxUnderwaterForwardSpeed;
S32 mcMaxUnderwaterBackwardSpeed;
S32 mcMaxUnderwaterSideSpeed;


Add the following line to line 417
void setMaxSpeed(S32 forward, S32 back, S32 side, S32 uwforward, S32 uwback, S32 uwside);
S32 getMaxForwardSpeed() { return mcMaxForwardSpeed; }
S32 getMaxBackwardSpeed() { return mcMaxBackwardSpeed; }
S32 getMaxSideSpeed() { return mcMaxSideSpeed; }
S32 getMaxUWForwardSpeed() { return mcMaxUnderwaterForwardSpeed; }
S32 getMaxUWBackwardSpeed() { return mcMaxUnderwaterBackwardSpeed; }
S32 getMaxUWSideSpeed() { return mcMaxUnderwaterSideSpeed; }


Open player.cc and Add the following into line 789
mcRunEnergyDrain = 0;
mcMinRunEnergy = 0;
mcMaxForwardSpeed = 0;
mcMaxBackwardSpeed = 0;
mcMaxSideSpeed = 0;
mcMaxUnderwaterForwardSpeed = 0;
mcMaxUnderwaterBackwardSpeed = 0;
mcMaxUnderwaterSideSpeed = 0;





STEP 2 - PLAYER SPEED ALTERATIONS

What we are doing here is really just the functions to send information to the server from
the console and to adjust the player movement.

Open player.cc and change the applicable lines at approx 1424 to the following

// Clamp water movement
if (move->y > 0)
{
if( mWaterCoverage >= 0.9 )
moveSpeed = getMax(getMaxUWForwardSpeed() * move->y,
getMaxUWSideSpeed() * mFabs(move->x));
else
moveSpeed = getMax(getMaxForwardSpeed() * move->y,
getMaxSideSpeed() * mFabs(move->x));
}
else
{
if( mWaterCoverage >= 0.9 )
moveSpeed = getMax(getMaxUWBackwardSpeed() * mFabs(move->y),
getMaxUWSideSpeed() * mFabs(move->x));
else
moveSpeed = getMax(getMaxBackwardSpeed() * mFabs(move->y),
getMaxSideSpeed() * mFabs(move->x));
}



Add the following functions in at approx line 3669

static void cSetMaxSpeed(SimObject *ptr, S32, const char **argv)
{
Player* obj = static_cast(ptr);
obj->setMaxSpeed(dAtof(argv[2]), dAtof(argv[3]), dAtof(argv[4]), dAtof(argv[5]), dAtof(argv[6]), dAtof(argv[7]));
}

static S32 cGetMaxForwardSpeed(SimObject *ptr, S32, const char **)
{
Player* obj = static_cast(ptr);
return obj->getMaxForwardSpeed();
}

static S32 cGetMaxBackwardSpeed(SimObject *ptr, S32, const char **)
{
Player* obj = static_cast(ptr);
return obj->getMaxBackwardSpeed();
}

static S32 cGetMaxSideSpeed(SimObject *ptr, S32, const char **)
{
Player* obj = static_cast(ptr);
return obj->getMaxSideSpeed();
}

static S32 cGetMaxUWForwardSpeed(SimObject *ptr, S32, const char **)
{
Player* obj = static_cast(ptr);
return obj->getMaxUWForwardSpeed();
}

static S32 cGetMaxUWBackwardSpeed(SimObject *ptr, S32, const char **)
{
Player* obj = static_cast(ptr);
return obj->getMaxUWBackwardSpeed();
}

static S32 cGetMaxUWSideSpeed(SimObject *ptr, S32, const char **)
{
Player* obj = static_cast(ptr);
return obj->getMaxUWSideSpeed();
}

void Player::setMaxSpeed(S32 forward, S32 back, S32 side, S32 uwforward, S32 uwback, S32 uwside)
{
if(forward >= 0)
mcMaxForwardSpeed = forward;
if(back >= 0)
mcMaxBackwardSpeed = back;
if(side >= 0)
mcMaxSideSpeed = side;
if(uwforward >= 0)
mcMaxUnderwaterForwardSpeed = uwforward;
if(uwback >= 0)
mcMaxUnderwaterBackwardSpeed = uwback;
if(uwside >= 0)
mcMaxUnderwaterSideSpeed = uwside;
if(isServerObject())
setMaskBits(CharMask);
}


The important lines in this last function are the last two....I will explain the purpose of these
later though :)

Add the following at line approx 3692

Con::addCommand("Player", "setMaxSpeed", cSetMaxSpeed, "obj.setMaxSpeed(forward, back, side, uwforward, uwback, uwside)", 3, 8);
Con::addCommand("Player", "getMaxForwardSpeed", cGetMaxForwardSpeed, "obj.getMaxForwardSpeed()", 2, 2);
Con::addCommand("Player", "getMaxBackwardSpeed", cGetMaxBackwardSpeed, "obj.getMaxBackwardSpeed()", 2, 2);
Con::addCommand("Player", "getMaxSideSpeed", cGetMaxSideSpeed, "obj.getMaxSideSpeed()", 2, 2);
Con::addCommand("Player", "getMaxUWForwardSpeed", cGetMaxUWForwardSpeed, "obj.getMaxUWForwardSpeed()", 2, 2);
Con::addCommand("Player", "getMaxUWBackwardSpeed", cGetMaxUWBackwardSpeed, "obj.getMaxUWBackwardSpeed()", 2, 2);
Con::addCommand("Player", "getMaxUWSideSpeed", cGetMaxUWSideSpeed, "obj.getMaxUWSideSpeed()", 2, 2);






STEP 3 - BITSTREAM ALTERATIONS

The first thing we need to do is add in a bitmask so that we can tell
when the ghost needs to send altered information to all the clients.
If we were to constantly update this information the effect on the
network would be horrendous so we only update when necessary

Open player.h and add the following in line 233 so it looks like so:
enum MaskBits {
ActionMask = Parent::NextFreeMask << 0,
MoveMask = Parent::NextFreeMask << 1,
ImpactMask = Parent::NextFreeMask << 2,
CharMask = Parent::NextFreeMask << 3,
NextFreeMask = Parent::NextFreeMask << 4
};

CharMask is our bitmask for the updates we are going to make.
Once again I am going to enphasise what we are going to do so that
you are completely clear.

The console command on the server console will alter a player variable
on the server player (ghost). When this occurs we tag our bitmask
(CharMask) which then allows the variables to be updated in the
bitstream. We do this because EVERYONE in the game must know what the
players speed is, otherwise you will get some nasty paradox effects
happening. (sorry if I am repeating myself but this is very important)

In the functions we created in Step 1 you will notice that we set
the setMaskBits(CharMask); but only when it was a serverObject (as
explained before, we only want to send info from the server to the
client). This is what tells the engine to update the information
in the bitstream. To do this we must send the changes in packupdate

Open player.cc and add the following at line 3390 (approx)

if (stream->writeFlag(mask & CharMask))
{
stream->writeInt(getMaxForwardSpeed(),5);
stream->writeInt(getMaxBackwardSpeed(),5);
stream->writeInt(getMaxSideSpeed(),5);
stream->writeInt(getMaxUWForwardSpeed(),5);
stream->writeInt(getMaxUWBackwardSpeed(),5);
stream->writeInt(getMaxUWSideSpeed(),5);
}


We have sent the data, now we need the clients to recieve it.

Add the following at approx line 3485
if (stream->readFlag())
{
S32 maxforwardspeed = stream->readInt(5);
S32 maxbackwardspeed = stream->readInt(5);
S32 maxsidespeed = stream->readInt(5);
S32 maxuwforwardspeed = stream->readInt(5);
S32 maxuwbackwardspeed = stream->readInt(5);
S32 maxuwsidespeed = stream->readInt(5);
setMaxSpeed(maxforwardspeed, maxbackwardspeed, maxsidespeed, maxuwforwardspeed, maxuwbackwardspeed, maxuwbackwardspeed);
}

As you can see from above, if there are no changes, only one bit is
sent.. a 0, stating that there will be no update of this information
If there are changes, a 1 is sent and then 30 bits sending the values
of our speeds. In this way, the TGE networking is extremely fast as
it only sends what is NEEDED to be sent.


SUMMARY
This is not an easy concept to grasp and, the first couple of times you
attempt to do this kind of thing you may find it takes a few hours of
mucking around to get it working correctly.

The basic command sequence.

Console command changes data
|
|
|
\/
Update player properties on server (ghost)
|
|
|
\/
Set bitmask
|
|
|
\/
Server sends data to clients in packupdate
|
|
|
\/
All clients recieve new data in unpackupdate
|
|
|
\/
clients write data to player properties

I hope this helps you guys out and, when your head explodes doing this,
dont blame me ;)

Enjoy :)

Daniel Neilsen

#1
05/13/2002 (10:56 pm)
This is extremely gracious of you to contribute so much work!

I hope to be able to repay this with some code submissions of my own in the coming months.

Thanks!

One issue is the line numbers don't match up with the code from Release_1_1_1 and some of the free form code snippets are hard to find, it would be helpful to mention which operation these code snippets to be altered are in.
#2
05/15/2002 (11:20 am)
Yep, that's true... would be great if you could add the exact location of the code alterations... line numbers don't help much if you altered these files before... didn't work for me yet, compiled fine after a while but crashed the engine ...
Anyhow, thanks for the code, I will give it another try once I got more time...
#3
05/15/2002 (11:43 am)
Well I am trying to do the same thing only in a differnt fashion.

I am creating a new sub-class of Player called
AtributedPlayer.

That way I can override the operations that I need to in AttributedPlayer and when I get a newer version of the source from CVS, I don't have a merging nightmare, plus this is the "correct" way to do this anyway, being as this is Object Oriented code to begin with.

I would be willing to collaborate and share what I can get done if since you are trying to do something very similar.
#4
06/04/2002 (2:40 pm)
Quote:To this end I developed this system which enables you to change player
abilitys, such as speed, energy usage, jump abilitys, etc, etc dynamically
through the use of console commands, used by the script engine.

Is it not possible to change most of these in the scripts, but copying player.cs to a new name, and modifying the variables?
#5
06/05/2002 (12:14 am)
No, quite impossible for a number of reasons.

1) You might have 20 different players using the one playerdata datablock. You want each individual players speeds to be different, not to change all of them at once.

2) If you alter the datablock midgame, it gets changed on the server...not on the clients. This will cause you to have sync problems AND still ahve the problem from 1 above.

What we are doing here is no longer using the values in the playerdata datablock for the run speed but instead have each player object with its own values.
#6
06/10/2002 (7:24 pm)
Ah so the important word is "dynamically". I understand now!
#7
12/29/2004 (8:12 pm)
Works well if you can get the code in the right places. ;)
#8
03/21/2005 (2:25 pm)
Anyone have a good ref for 1.3.0 as to where to put the code?
#9
06/19/2005 (10:37 pm)
in player.h

class Player: public ShapeBase
{
   typedef ShapeBase Parent;
protected:
   /// Bit masks for different types of events
   enum MaskBits {
      ActionMask   = Parent::NextFreeMask << 0,
      MoveMask     = Parent::NextFreeMask << 1,
      ImpactMask   = Parent::NextFreeMask << 2,
[b]
      CharMask     = Parent::NextFreeMask << 3,
      NextFreeMask = Parent::NextFreeMask << 4
[/b]
   };

   struct Range {
      Range(F32 _min,F32 _max) {
         min = _min;
         max = _max;
         delta = _max - _min;
      };
      F32 min,max;
      F32 delta;
   };

...

   Point3F mLastPos;             ///< Holds the last position for physics updates
   Point3F mLastWaterPos;        ///< Same as mLastPos, but for water

[b]
   // Player Speed Settings
   S32 mcRunEnergyDrain;
   S32 mcMinRunEnergy;
   S32 mcMaxForwardSpeed;
   S32 mcMaxBackwardSpeed;
   S32 mcMaxSideSpeed;
   S32 mcMaxUnderwaterForwardSpeed;
   S32 mcMaxUnderwaterBackwardSpeed;
   S32 mcMaxUnderwaterSideSpeed;
[/b]

...

   void onCameraScopeQuery(NetConnection *cr, CameraScopeQuery *);
   void writePacketData(GameConnection *conn, BitStream *stream);
   void readPacketData (GameConnection *conn, BitStream *stream);
   U32  packUpdate  (NetConnection *conn, U32 mask, BitStream *stream);
   void unpackUpdate(NetConnection *conn,           BitStream *stream);

[b]
   // Player Speed Settings
    void setMaxSpeed(S32 forward, S32 back, S32 side, S32 uwforward, S32 uwback, S32 uwside);
    S32 getMaxForwardSpeed() { return mcMaxForwardSpeed; }
    S32 getMaxBackwardSpeed() { return mcMaxBackwardSpeed; }
    S32 getMaxSideSpeed() { return mcMaxSideSpeed; }
    S32 getMaxUWForwardSpeed() { return mcMaxUnderwaterForwardSpeed; }
    S32 getMaxUWBackwardSpeed() { return mcMaxUnderwaterBackwardSpeed; }
    S32 getMaxUWSideSpeed() { return mcMaxUnderwaterSideSpeed; }
[/b]
};
#10
06/19/2005 (10:42 pm)
in player.cc

Player::Player()
{

...

   mBubbleEmitterTime = 10.0;
   mLastWaterPos.set( 0.0, 0.0, 0.0 );

[b]
   mcRunEnergyDrain = 0;
   mcMinRunEnergy = 0;
   mcMaxForwardSpeed = 0;
   mcMaxBackwardSpeed = 0;
   mcMaxSideSpeed = 0;
   mcMaxUnderwaterForwardSpeed = 0;
   mcMaxUnderwaterBackwardSpeed = 0;
   mcMaxUnderwaterSideSpeed = 0;
[/b]
}

...

void Player::updateMove(const Move* move)
{
   delta.move = *move;
      
   // Trigger images
   if (mDamageState == Enabled) {
      setImageTriggerState(0,move->trigger[0]);
      setImageTriggerState(1,move->trigger[1]);
   }

...



   // Desired move direction & speed
   VectorF moveVec;
   F32 moveSpeed;
   if (mState == MoveState && mDamageState == Enabled)
   {
      zRot.getColumn(0,&moveVec);
      moveVec *= move->x;
      VectorF tv;
      zRot.getColumn(1,&tv);
      moveVec += tv * move->y;
      
[b]
      // Clamp water movement
      if (move->y > 0)
      {
          if( mWaterCoverage >= 0.9 )
              moveSpeed = getMax(getMaxUWForwardSpeed() * move->y,
                                 getMaxUWSideSpeed() * mFabs(move->x));
          else
              moveSpeed = getMax(getMaxForwardSpeed() * move->y,
                                 getMaxSideSpeed() * mFabs(move->x));
      }
      else
      {
          if( mWaterCoverage >= 0.9 )
              moveSpeed = getMax(getMaxUWBackwardSpeed() * mFabs(move->y),
                                 getMaxUWSideSpeed() * mFabs(move->x));
          else
              moveSpeed = getMax(getMaxBackwardSpeed() * mFabs(move->y),
                                 getMaxSideSpeed() * mFabs(move->x));
      }
[/b]

      // Cancel any script driven animations if we are going to move.
      if (moveVec.x + moveVec.y + moveVec.z != 0 &&
          (mActionAnimation.action >= PlayerData::NumTableActionAnims
               || mActionAnimation.action == PlayerData::LandAnim))
         mActionAnimation.action = PlayerData::NullAnimation;
   }

...


U32 Player::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
{
   U32 retMask = Parent::packUpdate(con, mask, stream);

[b]
   if (stream->writeFlag(mask & CharMask))
   {
       stream->writeInt(getMaxForwardSpeed(),5);
       stream->writeInt(getMaxBackwardSpeed(),5);
       stream->writeInt(getMaxSideSpeed(),5);
       stream->writeInt(getMaxUWForwardSpeed(),5);
       stream->writeInt(getMaxUWBackwardSpeed(),5);
       stream->writeInt(getMaxUWSideSpeed(),5);
   }
[/b]

   if (stream->writeFlag((mask & ImpactMask) && !(mask & InitialUpdateMask)))
      stream->writeInt(mImpactSound, PlayerData::ImpactBits);

...


void Player::unpackUpdate(NetConnection *con, BitStream *stream)
{
   Parent::unpackUpdate(con,stream);
   
[b]
   if (stream->readFlag())
   {
       S32 maxforwardspeed = stream->readInt(5);
       S32 maxbackwardspeed = stream->readInt(5);
       S32 maxsidespeed = stream->readInt(5);
       S32 maxuwforwardspeed = stream->readInt(5);
       S32 maxuwbackwardspeed = stream->readInt(5);
       S32 maxuwsidespeed = stream->readInt(5);
       setMaxSpeed(maxforwardspeed, maxbackwardspeed, maxsidespeed, maxuwforwardspeed, maxuwbackwardspeed, maxuwbackwardspeed);
   }
[/b]

   if (stream->readFlag()) 
      mImpactSound = stream->readInt(PlayerData::ImpactBits);

...

   }
   F32 energy = stream->readFloat(EnergyLevelBits) * mDataBlock->maxEnergy;
   setEnergyLevel(energy);
}

[b]
static void cSetMaxSpeed(SimObject *ptr, S32, const char **argv)
{
    Player* obj = static_cast<Player*>(ptr);
    obj->setMaxSpeed(dAtof(argv[2]), dAtof(argv[3]), dAtof(argv[4]), dAtof(argv[5]), dAtof(argv[6]), dAtof(argv[7]));
}

static S32 cGetMaxForwardSpeed(SimObject *ptr, S32, const char **)
{
    Player* obj = static_cast<Player*>(ptr);
    return obj->getMaxForwardSpeed();
}

static S32 cGetMaxBackwardSpeed(SimObject *ptr, S32, const char **)
{
    Player* obj = static_cast<Player*>(ptr);
    return obj->getMaxBackwardSpeed();
}

static S32 cGetMaxSideSpeed(SimObject *ptr, S32, const char **)
{
    Player* obj = static_cast<Player*>(ptr);
    return obj->getMaxSideSpeed();
}

static S32 cGetMaxUWForwardSpeed(SimObject *ptr, S32, const char **)
{
    Player* obj = static_cast<Player*>(ptr);
    return obj->getMaxUWForwardSpeed();
}

static S32 cGetMaxUWBackwardSpeed(SimObject *ptr, S32, const char **)
{
    Player* obj = static_cast<Player*>(ptr);
    return obj->getMaxUWBackwardSpeed();
}

static S32 cGetMaxUWSideSpeed(SimObject *ptr, S32, const char **)
{
    Player* obj = static_cast<Player*>(ptr);
    return obj->getMaxUWSideSpeed();
}

void Player::setMaxSpeed(S32 forward, S32 back, S32 side, S32 uwforward, S32 uwback, S32 uwside)
{
    if(forward >= 0)
        mcMaxForwardSpeed = forward;
    if(back >= 0)
        mcMaxBackwardSpeed = back;
    if(side >= 0)
        mcMaxSideSpeed = side;
    if(uwforward >= 0)
        mcMaxUnderwaterForwardSpeed = uwforward;
    if(uwback >= 0)
        mcMaxUnderwaterBackwardSpeed = uwback;
    if(uwside >= 0)
        mcMaxUnderwaterSideSpeed = uwside;
    if(isServerObject())
        setMaskBits(CharMask);
}
[/b]

ConsoleMethod( Player, getState, const char*, 2, 2, "Return the current state name.")
{
   return object->getStateName();
}

...

void Player::consoleInit()
{
   Con::addVariable("pref::Player::renderMyPlayer",TypeBool, &sRenderMyPlayer);
   Con::addVariable("pref::Player::renderMyItems",TypeBool, &sRenderMyItems);

   Con::addVariable("Player::minWarpTicks",TypeF32,&sMinWarpTicks);
   Con::addVariable("Player::maxWarpTicks",TypeS32,&sMaxWarpTicks);
   Con::addVariable("Player::maxPredictionTicks",TypeS32,&sMaxPredictionTicks);
   
[b]
   Con::addCommand("Player", "setMaxSpeed", cSetMaxSpeed, "obj.setMaxSpeed(forward, back, side, uwforward, uwback, uwside)", 3, 8);
   Con::addCommand("Player", "getMaxForwardSpeed", cGetMaxForwardSpeed, "obj.getMaxForwardSpeed()", 2, 2);
   Con::addCommand("Player", "getMaxBackwardSpeed", cGetMaxBackwardSpeed, "obj.getMaxBackwardSpeed()", 2, 2);
   Con::addCommand("Player", "getMaxSideSpeed", cGetMaxSideSpeed, "obj.getMaxSideSpeed()", 2, 2);
   Con::addCommand("Player", "getMaxUWForwardSpeed", cGetMaxUWForwardSpeed, "obj.getMaxUWForwardSpeed()", 2, 2);
   Con::addCommand("Player", "getMaxUWBackwardSpeed", cGetMaxUWBackwardSpeed, "obj.getMaxUWBackwardSpeed()", 2, 2);
   Con::addCommand("Player", "getMaxUWSideSpeed", cGetMaxUWSideSpeed, "obj.getMaxUWSideSpeed()", 2, 2);
[/b]
}
#11
07/31/2005 (10:21 am)
How would this be done completely by script, like what Jarrod said? Also, how coudl it be implimented for things other than speed, like strength and accuracy. FOr my game I'm looking at:

Strenght(power of melee attacks/how much you can carry)
Agility(speed and jumping)
Accuracy(how often attacks hit people)

Is there just a way to script these as dynamic variables that can change in mid-game, like the HL1 mod Natural Selection.
#12
08/21/2005 (1:44 pm)
Oh, got it. And I made a a GUI that can set these variables =).
#13
07/28/2006 (10:00 am)
In making a GUI to use this, do I assume you'd make use of calls such as:

CommandToServer('setMaxSpeed', 1.5, 1.5, 1.5, 1.5, 1.5);

So that the server executes the command?
#14
08/18/2007 (1:57 pm)
Just wanted to add that this can be done throuugh scrip too, although this solution is much smoother.
#15
06/16/2009 (2:18 pm)
hey, i followed the tutorial to a T, recompiled and didn't get a single error or warning (which means give credit where credit is due, great code man!)

my problem is when i try to call the function it says it cannot find it...

i.e. in my code i have in my player.cs on the server side:
function giveReward(%this){
   ...
   %this.setMaxSpeed(25,25,25,25,25,25);
   ...
}

and test it, then look at the console and it says it couldn't find such function...

any help as to what i am doing wrong would be greatly appreciated