Game Development Community

Adding armor like quake.

by Richard Preziosi · in Torque Game Engine · 11/08/2009 (6:15 am) · 19 replies

So I'm looking to implement quake-like armor, something that absorbs damage before health is absorbed. So I figured I'd duplicate the health field, and script it where that has to reach 0 before health can be affected. It takes so long to rebuild, I figured I'd make sure I was on the right path before i screwed with things. Is damagelevel in the source code what health is? Cause I see a few places where damage level is tied in with energy and it's throwing me off.

#1
11/09/2009 (10:06 pm)
So I decided to try to do this through script with some script that I found in an older thread. Not sure how I'm going ot tie it to a progress bar, but I can figure that out later. Here is what I was trying to do.

player.cs
function Armor::damageSheild(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if((%this.hasSheild) && (%this.sheildPower > 0))
   {
      %this.sheildPower -= %damage;
      if(%this.sheildPower < 0)
      {
         %this.sheildPower = 0;
      }
   }
}

function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if (%obj.getState() $= "Dead")
      return;
   
   if((%obj.hasSheild) && (%obj.sheildPower > 0))
   {
      echo("shielded");
      %obj.damageSheild(%obj, %sourceObject, %position, %damage, %damageType);
   } else
   {
      echo("not shielded");
      %obj.applyDamage(%damage);
   }
   
      
   %location = "Body";

   // Deal with client callbacks here because we don't have this
   // information in the onDamage or onDisable methods
   %client = %obj.client;
   %sourceClient = %sourceObject ? %sourceObject.client : 0;

   if (%obj.getState() $= "Dead")
      %client.onDeath(%sourceObject, %sourceClient, %damageType, %location);
}

I tried setting hasShield and shieldPower in both gameConnection::CreatePlayer in game.cs, and I tried setting it in PlayerData(PlayerBody) datablock which is where health and energy are set, but no go.

Am I not allowed to create fields and set them like this without them being tied to anything in the source? Or am I just mixing something up somewhere.
#2
11/10/2009 (1:50 am)
That should work fine. The only downside to this script approach is the lack of networked fields. You'd need to use commandToClient to send information about the shield/armor to each client for display on the HUD, for example.

So why is it not working? Looks like you're getting confused with %this vs %obj. I made a few corrections here:

function Armor::damageSheild(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if((%obj.hasSheild) && (%obj.sheildPower > 0))
   {
      %obj.sheildPower -= %damage;
      if(%obj.sheildPower < 0)
      {
         %obj.sheildPower = 0;
      }
   }
}

function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if (%obj.getState() $= "Dead")
      return;
   
   if((%obj.hasSheild) && (%obj.sheildPower > 0))
   {
      echo("shielded");
      %this.damageSheild(%obj, %sourceObject, %position, %damage, %damageType);
   } else
   {
      echo("not shielded");
      %obj.applyDamage(%damage);
   }
   
      
   %location = "Body";

   // Deal with client callbacks here because we don't have this
   // information in the onDamage or onDisable methods
   %client = %obj.client;
   %sourceClient = %sourceObject ? %sourceObject.client : 0;

   if (%obj.getState() $= "Dead")
      %client.onDeath(%sourceObject, %sourceClient, %damageType, %location);
}

(without actually testing it.. I think i got that right)

See, %this refers in both functions to Armor. Since both function are defined in that namespace. Armor, if memory serves, is the class name assigned to the default player datablock PlayerBody. So you can think of Armor as a class for that particular player type. Whereas %obj refers to the specific instance of said player type.

So recap:
%obj refers to the specific Player instance of Armor type (defined be assigning the Player a dataBlock PlayerData which defined Armor as a className).
%this refers to Armor, since both function are defined as "Armor::functionName".

Furthermore, when you call "%this.function()", Torque will translate that to a corresponding call to "WhateverClassThisWas::function(%this)" where %this is the id of the same object instance. Follow?

BTW, you could simplify those functions a bit like so:
function Armor::damageSheild(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if((%obj.hasSheild) && (%obj.sheildPower > 0))
   {
      echo("shielded");
      %obj.sheildPower -= %damage;
      if(%obj.sheildPower < 0)
      {
         %obj.sheildPower = 0;
      }
      return true;
   }
   return false;
}

function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if (%obj.getState() $= "Dead")
      return;
   
   if(!%this.damageSheild(%obj, %sourceObject, %position, %damage, %damageType))
   {
      echo("not shielded");
      %obj.applyDamage(%damage);
   }
   
      
   %location = "Body";

   // Deal with client callbacks here because we don't have this
   // information in the onDamage or onDisable methods
   %client = %obj.client;
   %sourceClient = %sourceObject ? %sourceObject.client : 0;

   if (%obj.getState() $= "Dead")
      %client.onDeath(%sourceObject, %sourceClient, %damageType, %location);
}

Also, I imagine you are aware that you will want to actually initialize %obj.hasSheild and %obj.shieldPower somewhere. Probably when the player is spawned. All undeclared/undefined variables in TorqueScript will initially have a value of ""(empty string)/0/false. Unless you want the player to start with no shield of course.
#3
11/10/2009 (7:38 am)
Thanks Scott, however those codes give me an error in console, unable to find object, so I'm still unable to get it to work, it may be because it's not networked properly as you stated.

However I thought it should work in multiplayer, since i'm setting hasShield and shieldPower in game.cs in CreatePlayer. If I was wrong to assume that, please feel free to let me know.
#4
11/10/2009 (12:10 pm)
Well ok, so what's the full error message say? Line number, etc? It's a simple error, I'm sure.

It's not a network issue. This will technically function correctly, as damage is controlled server-side. Where you will run into problems with the lack of networked values is when (and only when) it comes to rendering client side effects; like drawing shield sparks when the shield takes a hit, or displaying the current shield value on a client HUD. Still, the shield value will go down as damage occurs and the player will take damage once the shield fails, as expected, because all that is handled server-side.
#5
11/10/2009 (7:43 pm)
The error is just how I am setting the shield and shield power. In my game.cs I was using
%obj.hasShield = true;
%obj.shieldPower = 1000;
These lines are what is giving me problems, i've tried rewriting them as every %something i can think of and no go. Which leads me to believe that I'm missing another piece of code that needs to be added somewhere.
#6
11/10/2009 (8:21 pm)
Ok. In the function GameConnection::createPlayer in game.cs? %player is the object you want. %player.hasShield = true; etc. %player refers to the Player instance that should ultimately be the same object that %obj refers to when Armor::damage is called.
#7
11/10/2009 (9:14 pm)
Ok cool, I was thinking %player was correct as you set inventory and mounted item with %player. That gets rid of the error, but I'm still echoing "not shielded". Does hasShield and shieldPower need to be described further in a function or datablock somewhere? Or am I just screwing the pooch bigtime on this?

Thanks so far for all the help.
#8
11/10/2009 (10:29 pm)
Quote:Does hasShield and shieldPower need to be described further in a function or datablock somewhere?

No. That should work. Try ...

Wait... doh! Check your spelling. I before E. You spelled shield correctly in some places, and misspelled it sh(ei)ld in others. Correct all the spelling mistakes and you should be all set.

That's the big problem with TorqueScript, or really with any language that does not require variable declaration. There's no way for the compiler/interpreter to warn you when you made a typo in a variable name. :(
#9
11/11/2009 (1:43 am)
HAHA, well that is embarassing, in my defense

Quote:So I decided to try to do this through script with some script that I found in an older thread.

Atleast I remembered all the ";"s this time.

Works great now, thanks Scott.
#10
11/12/2009 (3:50 am)
Maybe you can help me with one last step on this. I was implementing tying it to a guiAdvBarHud that I implemented. It works for the most part, except right now it's just depleting the whole bar because I'm not super sure how to make it take away only the damage.

here is what I have, what's in bold is what I'm not sure about so I left it blank while posting here.
function UpdateArmorProgressBar(%this, %obj, %sourceObject, %position, %damage, %damageType)   
{   
   ArmorProgressBar.FillTo([b]this is where the percentage needs to come in, but i'm not sure how to write it[/b]);   
   schedule(100, 0, UpdateArmorProgressBar, %obj);   
}

I had the bold section originally set to
ArmorProgressBar.FillTo(1.0 - %damage());
But that just depletes the whole thing at once.

If anyone has any ideas would appreciate it. Thanks.
#11
11/12/2009 (11:31 am)
Couple things:

First of all, in "ArmorProgressBar.FillTo(1.0 - %damage());" %damage is not a function, so get the () out of there.

Second, what is the value range for %damage? Has %damage already been scaled and clamped to a range 0 to 1? Assuming you left the default guiAdvBarHud values "minValue" and "maxValue" set to 0 and 1 respectively then you'll need %damage to be in the range 0 to 1. If you're just calling UpdateArmorProgressBar() from Armor::damageShield() (which I'm guessing you are?) then it most likely has not been scaled or clamped. You'll want to define the maximum shield strength so you can find the percentage of armor strength remaining as "shieldPower / maxShieldPower".

OR you could set maxValue of your guiAdvBarHud to the max strength of the armor shield, then you can simply call "ArmorProgressBar.drain(%damage)" without scaling or clamping the damage value.

NOTE that if you are just calling UpdateArmorProgressBar from damageShield, then this system will NOT work across a network. Because, you are setting gui elements directly from server variables. It will work locally so it will work for a strictly singleplayer game, but not multiplayer.
#12
11/12/2009 (11:42 am)
Yeah I was calling it in damageShield, was taking baby steps getting it to funciton locally, then later get it to function networked.

I have minValue set to 0 and maxValue set to 100 for the guiAdvBarHud. I'll try out the .drain method, as that seems like it's a simplified version.

As far as networkability goes, where should i be putting the commandToClient for it to function properly?

Thanks again.
#13
11/12/2009 (2:31 pm)
Ok. So, commandToClient needs to be called by the server (obviously). You can call it whenever you want, depending on when you want the update to occur. The simplest approach would probably be to call commandToClient as a result of applying damage. i.e. from damageShield like so:

function Armor::damageSheild(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if((%obj.hasSheild) && (%obj.sheildPower > 0))
   {
      echo("shielded");
      %obj.sheildPower -= %damage;
      if(%obj.sheildPower < 0)
      {
         %obj.sheildPower = 0;
      }
      // inform client of change
      if (isObject(%obj.client))
         commandToClient(%obj.client, updateArmorBar, %obj.shieldPower);

Easy, but the downside there is that the update is sent to the client regardless of how little or how often the value is changed. Which may result in more network traffic than necessary, depending on the nature of your game. If that is a concern, you may want to consider implementing a system to send updates on a schedule, or only when the value has changed beyond some preset threshold. Perhaps something like this:

function Armor::sendShieldUpdate(%this, %obj)
{
   if (!isObject(%obj) || !isObject(%obj.client)) return;
   if (mAbs(%obj.lastSentShieldPower - %obj.shieldPower) > 1) // <- threshold of delta to trigger update
   {
      commandToClient(%obj.client, updateArmorBar, %obj.shieldPower);
      %obj.lastSentShieldPower = %obj.shieldPower;
   }
   %obj.shieldUpdateTimer = %this.schedule(256, sendShieldUpdate, %obj);
}

function Armor::damageSheild(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if((%obj.hasSheild) && (%obj.sheildPower > 0))
   {
      echo("shielded");
      %obj.sheildPower -= %damage;
      if(%obj.sheildPower < 0)
      {
         %obj.sheildPower = 0;
      }
      // inform client of change
      if (isObject(%obj.client) && !isEventPending(%obj.shieldUpdateTimer))
         %obj.shieldUpdateTimer = %this.schedule(256, sendShieldUpdate, %obj);
// ... etc

Then of course you'll need to create a function on the client to receive the command.

function clientCmdUpdateArmorBar(%value)
{
   ArmorProgressBar.FillTo(%value);
}

Doesn't matter where that function goes, so long as it is in some file that gets loaded as part of the client initialization.

Now, note that I'm sending an absolute value (shieldPower) there, rather than a relative value (%damage). Hence the call to "fillTo" and not "drain". Why? Well, I figure since commandToClient is going to send the value in its full uncompressed 32 bit form either way, we might as well send an absolute value which has no chance of getting "out of sync". Though commandToClient IIRC is supposed to be sent reliably and in order(?) so you could probably get away with relative values if you wanted to.

(edit: oops. fixed missing parenthesis on line 6, second code block)
#14
11/12/2009 (6:40 pm)
Thank you for your prompt replies Scott, wont' be home for another few hours, but it looks like you got me up and running. Will post again if I have any more problems. You have been very generous with your posts, thanks again.
#15
11/13/2009 (6:11 am)
So here's what I ended up with
function Armor::damageShield(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if((%obj.hasShield) && (%obj.shieldPower > 0))
   {
      echo("shielded");
      %obj.shieldPower -= %damage;
      if(%obj.shieldPower < 0)
      {
         %obj.shieldPower = 0;
      }
      // inform client of change   
      if (isObject(%obj.client) && !isEventPending(%obj.shieldUpdateTimer))   
         %obj.shieldUpdateTimer = %this.schedule(256, sendShieldUpdate, %obj);  
      return true;
   }
   return false;
}

function Armor::sendShieldUpdate(%this, %obj)
{
   if (!isObject(%obj) || !isObject(%obj.client)) return;
   if (mAbs(%obj.lastSentShieldPower - %obj.shieldPower) > 1) // <- threshold of delta to trigger update
   {
      commandToClient(%obj.client, UpdateArmorProgressBar, %obj.shieldPower);
      %obj.lastSentShieldPower = %obj.shieldPower;
   }
   %obj.shieldUpdateTimer = %this.schedule(256, sendShieldUpdate, %obj);
}

function UpdateArmorProgressBar(%this, %obj, %sourceObject, %position, %damage, %damageType)   
{   
   ArmorProgressBar.FillTo(1.0 - %damage);   
   schedule(100, 0, UpdateArmorProgressBar, %obj);   
}

function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if (%obj.getState() $= "Dead")
      return;
   
   if(!%this.damageShield(%obj, %sourceObject, %position, %damage, %damageType))
   {
      echo("not shielded");
      %obj.applyDamage(%damage);
   }
   
      
   %location = "Body";

   // Deal with client callbacks here because we don't have this
   // information in the onDamage or onDisable methods
   %client = %obj.client;
   %sourceClient = %sourceObject ? %sourceObject.client : 0;

   if (%obj.getState() $= "Dead")
      %client.onDeath(%sourceObject, %sourceClient, %damageType, %location);
}

I added the client command function to client.cs as it seemed the most appropriate in the init.cs, although I think playgui.cs would be a good choice as well.

I'm getting one error now though "Remote Command Error = command must be a tag." If I fix this before you respond I'll post my fix, just not fimiliar with commandtoClients and whatever tags are.

Thanks again for all the help, big thanks going to you in my credits.

Edit: Ok I got that error to go away, needed to put 's into the command to client.
commandToClient(%obj.client, 'UpdateArmorProgressBar', %obj.shieldPower);
Works like a charm now, thank you so much Scott, like I said big thanks to you in the credits.
#16
11/13/2009 (6:52 am)
Last thing if you have the time, ha, promise this time. I was trying to get the bar to fill back to 100% by updating it in spawnPlayer like this.
function GameConnection::spawnPlayer(%this)
{
   // Combination create player and drop him somewhere
   %spawnPoint = pickSpawnPoint();
   %this.createPlayer(%spawnPoint);
   %shieldPower = 100;
   commandToClient(%obj.client, 'UpdateArmorProgressBar', %shieldPower);
}

No go though
#17
11/13/2009 (12:29 pm)
No go indeed, because %obj was never defined in that scope. In TorqueScript, variables prefixed with % are "local". They exist only within the scope of a single function. Observe in that codeblock how "%spawnPoint" is defined on line 4, so it can be referenced on line 5. However, as soon as the function spawnPlayer() ends, %spawnPoint will cease to exist.

Variables prefixed with $ on the other hand, are global. They persist for the duration of the program's execution.

Regardless.. in that particular case the client you are looking to command IS the GameConnection on which spawnPlayer() was called. So simply replace "%obj.client" with "%this" in line 7 and you're good to go.

... although, just so you are aware, setting %shieldPower there will not set the shieldPower field of the Player which gets referenced later in damageShield etc.
#18
11/13/2009 (1:19 pm)
Thanks again Scott, I believe I am fully done with the implementation of the armor bar thanks to you, tested quite a bit and works well.

As far as it not really setting the shield value for the referenced player, i fully understand that, as that command's purpose is solely just to reset the GUI element.

Again thanks very much, you've been most kind and generous.
#19
11/13/2009 (1:31 pm)
ok, cool. Happy to help.