Making the bot a client
by Dane Nielsen · in Torque Game Engine · 09/02/2006 (1:08 am) · 28 replies
I'm new to Torque and over the past few days I have been looking through the fps starter kit. Now upon looking at scoring I have found that it keeps track of client scores. This makes it so bots aren't on the list and don't count for kills. I was wondering if there's a way to edit the code to make the bot a client. If someone would help me out it would really help me understand how this is writen alot better and also it would make it so I could easily add bots later if I wanted to.
Thanks
Thanks
#2
in TGE, "client" means an actual person connected over an actual network connection,
but "player" means either an actual person or a bot.
much easier to modify the scoring system to be based on players instead of clients.
tip: if you've got a client object in script, eg "%client", then %client.player is the player object associated with that client connection.
09/02/2006 (9:19 am)
I'm with james.in TGE, "client" means an actual person connected over an actual network connection,
but "player" means either an actual person or a bot.
much easier to modify the scoring system to be based on players instead of clients.
tip: if you've got a client object in script, eg "%client", then %client.player is the player object associated with that client connection.
#3
http://www.garagegames.com/mg/forums/result.thread.php?qt=29039
http://tdn.garagegames.com/wiki/TGB/ScriptTutorials/ScoreTutorial
09/02/2006 (9:55 am)
You can add scoring on onDeath, onDamage, and respawn function. There alot ways to do it. Here few links to get you start. You might want do search on scoring.http://www.garagegames.com/mg/forums/result.thread.php?qt=29039
http://tdn.garagegames.com/wiki/TGB/ScriptTutorials/ScoreTutorial
#4
09/02/2006 (12:46 pm)
Thanks
#5
Funny you should ask. I was stumbling through the starter.fps as a tutorial as well and was looking at the same problem. It correctly scored suicides but not kills.
After some log watching and code digging I found that call was failing because the bot 'client' is null and therefor all calls client.abc will fail. So I added a check to play.cs to test for client being equal to "" and then incrementing the score appropriately.
Also notice I corrected (I THINK) the call to 'onDeath' because it had too few elements in it as it was.
I also altered Game.cs slightly to make the suicide check work with the new call:
I in no way guarantee that this code is correct but it works for me. The kill player 1 killed player 2 message all is not displayed but the scoring works. (The message could easily be where the echo statement is.) I have only tested it on 1 player 1 bot set-up so it is possible that with more players it will not work appropriately.
I have also added the rocket launcher to the demo (poor Kork) and I'm currently working on trying to get water damage to work. If you invoke it - even with zero damage - you die instantly. I've also toyed with adding weapon switching and maybe changing the footprint puffs to change color with the terrain but that is down on the list. Let me know what you're working on and how your progress is...
=Tod
09/02/2006 (9:14 pm)
@DaneFunny you should ask. I was stumbling through the starter.fps as a tutorial as well and was looking at the same problem. It correctly scored suicides but not kills.
After some log watching and code digging I found that call was failing because the bot 'client' is null and therefor all calls client.abc will fail. So I added a check to play.cs to test for client being equal to "" and then incrementing the score appropriately.
if (%obj.getState() $= "Dead"){
if (%client !$= ""){ //Call 'onDeath if client exists
echo("Death with client" @ %client);
%client.onDeath(%this, %sourceObject, %sourceClient, %damageType, %location);
}
else { //Otherwise it was a serverside object - increment the person score who inflicted the damage
echo("Congrats! You killed a bot!");
%sourceObject.client.incScore(1);
}
}Also notice I corrected (I THINK) the call to 'onDeath' because it had too few elements in it as it was.
I also altered Game.cs slightly to make the suicide check work with the new call:
if (%damageType $= "Suicide" || %sourceClient.client == %this) { //altered this call so suicide check was on .client
%sourceClient.client.incScore(-1);
messageAll('MsgClientKilled','%1 takes his own life!',%this.name);
}I in no way guarantee that this code is correct but it works for me. The kill player 1 killed player 2 message all is not displayed but the scoring works. (The message could easily be where the echo statement is.) I have only tested it on 1 player 1 bot set-up so it is possible that with more players it will not work appropriately.
I have also added the rocket launcher to the demo (poor Kork) and I'm currently working on trying to get water damage to work. If you invoke it - even with zero damage - you die instantly. I've also toyed with adding weapon switching and maybe changing the footprint puffs to change color with the terrain but that is down on the list. Let me know what you're working on and how your progress is...
=Tod
#6
Through echo-ing I was able to determine the lakes in starter.fps are "ocean water".
Also changes to ShapeBase that actually makes the call:
This gets damage to start and continue as long as you are in the water. The trouble is it doesn't stop when you leave the water. To fix this changes to player.cs:
And changes to ShapeBase.
As it says in the comments it assumes one type of liquid at a time but that should work for most situations. Because we're passing "0" for %this.damage call in ShapeBase the death is unattributed so a more elegant solution would include a "environment" or "elements" cause of death... or at least call it self-inflicted.
Also there is also a degree of immersion in the "liquid" calls so you could hook in under-liquid as opposed to just in-liquid but I think I'll do something else now.
=Tod
09/04/2006 (11:28 am)
As a follow-up I did finally manage to get the starter.fps enter and exit liquid damage to work and learned quite a bit about script<->engine interface in the process - which is really the point.Through echo-ing I was able to determine the lakes in starter.fps are "ocean water".
//Declaration
$DamageOcean = 3.0; //Get out quick!
.
.
function Armor::onEnterLiquid(%this, %obj, %coverage, %type)
{
switch(%type)
{
case 0: //Water
case 1: //Ocean Water
//My Code here - tkk
%obj.setDamageDt( %obj, $DamageOcean, "Ocean Water");
case 2: //River Water
case 3: //Stagnant Water
case 4: //Lava
%obj.setDamageDt(%this, $DamageLava, "Lava"); //Original calls
case 5: //Hot Lava
.
.
}
}Also changes to ShapeBase that actually makes the call:
function ShapeBase::setDamageDt(%this, %obj, %damageAmount, %damageType)
{
if (%obj.getState() !$= "Dead") {
%this.damage( 0, "0 0 0", %damageAmount, %damageType);
//Changed below line - added %obj so it comes back with the scheduled call - tkk
%obj.damageSchedule = %obj.schedule(50, "setDamageDt", %obj, %damageAmount, %damageType);
}
else
%obj.damageSchedule = "";
}This gets damage to start and continue as long as you are in the water. The trouble is it doesn't stop when you leave the water. To fix this changes to player.cs:
//Added type to this function to match engine call... -tkk
function Armor::onLeaveLiquid(%this, %obj, %type)
{
%obj.clearDamageDt(%obj, %type);
}And changes to ShapeBase.
function ShapeBase::clearDamageDt( %obj, %type)
{
//This should probably check type because leaving ANY liquid would probably turn it off...
//But for now we'll assume only one type of liquid/damage at a time -tkk
if (%obj.damageSchedule !$= "") {
cancel(%obj.damageSchedule);
%obj.damageSchedule = "";
}
}As it says in the comments it assumes one type of liquid at a time but that should work for most situations. Because we're passing "0" for %this.damage call in ShapeBase the death is unattributed so a more elegant solution would include a "environment" or "elements" cause of death... or at least call it self-inflicted.
Also there is also a degree of immersion in the "liquid" calls so you could hook in under-liquid as opposed to just in-liquid but I think I'll do something else now.
=Tod
#7
"enter and exit liquid damage" was never broken. It's been working fine for me and I haven't changed a thing.
09/04/2006 (11:33 am)
What on earth have you done? =)"enter and exit liquid damage" was never broken. It's been working fine for me and I haven't changed a thing.
#8
I added:
And upon entering the water you either die instantly or your health is instantly at zero. Even with $DamageOceanWater =0.0 this happened.
The console also kicks out errors indicating that %obj is null or not what it should be.
Now I am new to Torque but it seems that the variables were being passed incorrectly. %obj was null and damage did not correlate to $DamageOceanWater - as if the obj was getting passed as damage so it was always an insanely high number for damage. By echoing all the variables through each call - I was able to get the correct variables in the correct place - changes noted above - and it started working and actually scheduling and calling back with damage updates.
By making the changes above I was able to follow the variables through and finally get $DamageOceanWater variable passed through and "Ocean Water" as type. Also schedule was never getting called because %obj was meaningless - it simply a one time damage hit uncorrelated to anything.
I don't know if it is platform differences but I just re-enacted it to confirm that 1.4u (latest SDK) build acts this way out of the box.
Anyone else confirm/refute the working state of OnEnterLiquid calls especially on the Mac under 1.4u?
Thanks,
=Tod
09/04/2006 (12:11 pm)
I'm running SDK build 1.4u compiled under XCode 2.4 running under OS X 10.4.7 and it doesn't work at all. I just reloaded my archived starter.fps to confirm this.I added:
$DamageOceanWater = 0.01;
.
.
case 0: //Water
case 1: //Ocean Water
%obj.setDamageDt(%this, $DamageOceanWater, "Ocean Water");
case 2: //River Water
case 3: //Stagnant Water
case 4: //Lava
%obj.setDamageDt(%this, $DamageLava, "Lava");And upon entering the water you either die instantly or your health is instantly at zero. Even with $DamageOceanWater =0.0 this happened.
The console also kicks out errors indicating that %obj is null or not what it should be.
Quote:
Mapping string: Kork to index: 13
starter.fps/server/scripts/shapeBase.cs (35): Unable to find object: '' attempting to call function 'getState'
starter.fps/server/scripts/shapeBase.cs (37): Unable to find object: '' attempting to call function 'schedule'
Now I am new to Torque but it seems that the variables were being passed incorrectly. %obj was null and damage did not correlate to $DamageOceanWater - as if the obj was getting passed as damage so it was always an insanely high number for damage. By echoing all the variables through each call - I was able to get the correct variables in the correct place - changes noted above - and it started working and actually scheduling and calling back with damage updates.
By making the changes above I was able to follow the variables through and finally get $DamageOceanWater variable passed through and "Ocean Water" as type. Also schedule was never getting called because %obj was meaningless - it simply a one time damage hit uncorrelated to anything.
I don't know if it is platform differences but I just re-enacted it to confirm that 1.4u (latest SDK) build acts this way out of the box.
Anyone else confirm/refute the working state of OnEnterLiquid calls especially on the Mac under 1.4u?
Thanks,
=Tod
#9
09/04/2006 (1:12 pm)
Edit: I misread. Enter/Exit liquid works perfectly, but not the damaging functions.
#10
Can you do me a favor and pop open ShapeBase.cs and double check the code?
Because here is my unaltered version and unless there is some magic %this = %obj then the %obj calls in this routine are occurring on a non-existent variable and the same is true for clearDamageDt.
Now I am just learning Torque script so maybe this is an interchangeable convention I'm just learning about here but from the rest of my programming experience it looks wrong - %obj looks undefined for this subroutine.
Thanks for the help,
=Tod
09/04/2006 (1:39 pm)
Hrmmm... the only other version I have is an older version of 1.4 from CVS. I just ran that one and the same behavior - obj is "" and errors in the console.Can you do me a favor and pop open ShapeBase.cs and double check the code?
function ShapeBase::setDamageDt(%this, %damageAmount, %damageType)
{
// This function is used to apply damage over time. The damage
// is applied at a fixed rate (50 ms). Damage could be applied
// over time using the built in ShapBase C++ repair functions
// (using a neg. repair), but this has the advantage of going
// through the normal script channels.
if (%obj.getState() !$= "Dead") {
%this.damage(0, "0 0 0", %damageAmount, %damageType);
%obj.damageSchedule = %obj.schedule(50, "setDamageDt", %damageAmount, %damageType);
}
else
%obj.damageSchedule = "";
}
function ShapeBase::clearDamageDt(%this)
{
if (%obj.damageSchedule !$= "") {
cancel(%obj.damageSchedule);
%obj.damageSchedule = "";
}
}Because here is my unaltered version and unless there is some magic %this = %obj then the %obj calls in this routine are occurring on a non-existent variable and the same is true for clearDamageDt.
Now I am just learning Torque script so maybe this is an interchangeable convention I'm just learning about here but from the rest of my programming experience it looks wrong - %obj looks undefined for this subroutine.
Thanks for the help,
=Tod
#11
You were right; the damage on entering liquid function was not functioning properly.
Here's how I would fix the problem:
shapeBase.cs
Note that my fix also fixes the transmission of %damageType to the Armor::damage function.
To test, find the function and place an echo.
You will find that it will correctly report what type of water we died in.
Example:
player.cs
This ties in with the GameConnection::onDeath function, found in game.cs which is
useful for displaying custom death messages and scoring.
game.cs
For you to do:
If you look closely you'll see that %damLoc (damage location) is also broken.
This should be reporting head, torso, legs and which direction you were hit in.
At the moment it's being brute forced to "body".
See if you can fix it, might be a good learning experience.
-Tim.
09/05/2006 (2:44 am)
I do apologize,You were right; the damage on entering liquid function was not functioning properly.
Here's how I would fix the problem:
shapeBase.cs
function ShapeBase::setDamageDt(%this, %damageAmount, %damageType)
{
// This function is used to apply damage over time. The damage
// is applied at a fixed rate (50 ms). Damage could be applied
// over time using the built in ShapBase C++ repair functions
// (using a neg. repair), but this has the advantage of going
// through the normal script channels.
if (%this.getState() !$= "Dead") {
%this.damage(0, "0 0 0", %damageAmount, %damageType);
%this.damageSchedule = %this.schedule(50, "setDamageDt", %damageAmount, %damageType);
}
else
%this.damageSchedule = "";
}
function ShapeBase::clearDamageDt(%this)
{
if (%this.damageSchedule !$= "") {
cancel(%this.damageSchedule);
%this.damageSchedule = "";
}
}player.cs// Damage Rate for entering Liquid
$Damage::Water = 1;
$Damage::Lava = 2;
$Damage::QuickSand = 3;
function Armor::onEnterLiquid(%this, %obj, %coverage, %type)
{
switch(%type)
{
case 0: //Water
%obj.setDamageDt($Damage::Water, "Water");
case 1: //Ocean Water
%obj.setDamageDt($Damage::Water, "Water");
case 2: //River Water
%obj.setDamageDt($Damage::Water, "Water");
case 3: //Stagnant Water
%obj.setDamageDt($Damage::Water, "Water");
case 4: //Lava
%obj.setDamageDt($Damage::Lava, "Lava");
case 5: //Hot Lava
%obj.setDamageDt($Damage::Lava, "Lava");
case 6: //Crusty Lava
%obj.setDamageDt($Damage::Lava, "Lava");
case 7: //Quick Sand
%obj.setDamageDt($Damage::QuickSand, "Quick Sand");
}
}
function Armor::onLeaveLiquid(%this, %obj, %type)
{
%obj.clearDamageDt();
}Note that my fix also fixes the transmission of %damageType to the Armor::damage function.
To test, find the function and place an echo.
You will find that it will correctly report what type of water we died in.
Example:
player.cs
function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
echo(%damageType);
if (%obj.getState() $= "Dead")
return;
%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);
}This ties in with the GameConnection::onDeath function, found in game.cs which is
useful for displaying custom death messages and scoring.
game.cs
function GameConnection::onDeath(%this, %sourceObject, %sourceClient, %damageType, %damLoc)
{
// Clear out the name on the corpse
%this.player.setShapeName("");
// Switch the client over to the death cam and unhook the player object.
if (isObject(%this.camera) && isObject(%this.player)) {
%this.camera.setMode("Corpse",%this.player);
%this.setControlObject(%this.camera);
}
%this.player = 0;
// Doll out points and display an appropriate message
if (%damageType $= "Suicide" || %sourceClient == %this) {
%this.incScore(-1);
messageAll('MsgClientKilled','%1 takes his own life!',%this.name);
}
else {
%sourceClient.incScore(1);
messageAll('MsgClientKilled','%1 gets nailed by %2!',%this.name,%sourceClient.name);
if (%sourceClient.score >= $Game::EndGameScore)
cycleGame();
}
}For you to do:
If you look closely you'll see that %damLoc (damage location) is also broken.
This should be reporting head, torso, legs and which direction you were hit in.
At the moment it's being brute forced to "body".
See if you can fix it, might be a good learning experience.
-Tim.
#12
09/05/2006 (2:53 am)
@Tod: By the way because you mentioned it above somewhere. You should check for the client object not with "", but with a isObject(%client) test. It will return true if %client is valid.
#13
Example (out of the head):
Well, at least it works great for me. Maybe it helps you too.
09/05/2006 (4:11 am)
@Dane: As for you original question: You're right, counting bots like that is not working proper because AI players don't have a client object and the scoring is saved via the client object. What I did (and it works very well) is at first creating fake client objects in script by creating ScriptObjects. Each fake client (now a script object), gets a "player" variable, an "ai" variable and a "score" variable. Now I modified the game.cs to check at every place where "client" is referenced, if it is an AI object or not. Doing it this way makes it easy for me to have the same handling like real players and (at least for me) the advantage that you will now have persistent data for each bot! Example (out of the head):
// create one fake client object for an AI player. need to do this for each AI player we create!
%botClient = new ScriptObject()
{
ai = true;
player = 0;
score = 0;
// add here anything else regarding AI client stuff. example: skin name
};
// now create the bot
%yourAIPlayer = new AIPlayer(bla bla...)
{
...
client = %botClient; // forward reference to client object
...
};
// now make the back reference
%botClient.player = %yourAIPlayer;
...
continue your AI creation stuff.later in the game.cs code (as example):
function GameConnection::onDeath(%this, %sourceObject, %sourceClient, %damageType, %damLoc)
{
...
if (%sourceClient.ai) // checking for the manually set AI flag (ai = true, a bit above)
{
// this is an AI script object!
// do something.
}
...
}
function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
...
if (%obj.client.ai)
{
// is AI player/client!
%obj.client.score--; // as example
// send now to all players the new score
messageAll(...);
} else
{
// do here human player stuff...
}
}Well, at least it works great for me. Maybe it helps you too.
#14
09/05/2006 (4:37 am)
Thank you, this helps me alot.
#15
09/05/2006 (5:03 am)
If someone has an in-deep question about this fake client thing, don't hesitate to ask or drop me a mail. In the newest version I also added the fake clients to the ClientGroup Simset and gave the fake client some dummy member functions that are needed to treat it like a real client. Works great so far for me.
#16
Thanks.
I see the need for creating the scriptObject and using
I'm curious, I thought I remember seeing a ConsoleMethod
I also remember seeing AIClient and AIConnection in the Source[but not as scripts]...this has got me very curious how these stock items are never really used for what seems like this purpose???
I ask because I had tried the above code[not Martin's] and got it to indeed count any bot I killed to the ScoreBoard & no messages; but it also didn't deduct when I suicided and if I got killed by my own radiusDamage, I got a point for that...!
09/05/2006 (6:32 am)
Lol...I have many. This is a great Thread and is teaching me what I was continually doing wrong trying to accomplish the same thing. I'm and artist and scripting bogs me down sometimes[then I go off and animate a shape...:)]. I took the Aiguard and it has become my scripted AI[tried creating a list of ScriptObject bots to cycle thru...but, failure]...that wasn't too bad/hard, and I got a name and skin choice to work, but that was as far I as could get. This is helping me out and would be a great HowTo to add to the TDN site & Resource here, detailing the steps necessary[on a stock TGE, would be awesome,;)].Thanks.
I see the need for creating the scriptObject and using
if (%sourceClient.ai)...
I'm curious, I thought I remember seeing a ConsoleMethod
isAIControlled & %isAI...how would/wouldn't using that accomplish the same thing???
I also remember seeing AIClient and AIConnection in the Source[but not as scripts]...this has got me very curious how these stock items are never really used for what seems like this purpose???
I ask because I had tried the above code[not Martin's] and got it to indeed count any bot I killed to the ScoreBoard & no messages; but it also didn't deduct when I suicided and if I got killed by my own radiusDamage, I got a point for that...!
#17
That's the way I determine names for player scoring between real players & bots.
09/05/2006 (7:21 am)
if (%obj.client.isAIControlled())
%objName = %obj.getDatablock().getName();
else
%objName = getTaggedString(%obj.client.name);That's the way I determine names for player scoring between real players & bots.
#18
09/05/2006 (7:25 am)
@Rex: Yes, the AIClient class has those methods like isAiControlled. But the lineif (%sourceClient.ai)does in this case refer to the self-created client object. So I'm checking in that case not the player object (from which we know it is ai controlled), but if the client object is the self created one or if it is a "real" one.
#19
09/05/2006 (7:27 am)
Forgot to mention: I played around a lot with AIClient - but it doesn't like me very much. I wasn't succesfull getting it working with today's AIPlayer class.
#20
@Tim: does that snippet mean that you are indeed using AIClient, etal or is that method part of AIPlayer?? Or have you compiled some extraness into the stock AI provided??
09/05/2006 (8:06 am)
@Martin:aaaahhhhh...I see, I guess that's why noone mentions those classes/objects. Just that I've searched the terms and the best description I got was from GG's Tim's .plan from eons ago, announcing the creation of those two classes and the subsequent creation of the AIPlayer??? It's starting to make a 'little' sense...but also proves why simply 'searching' old Threads isn't always the means to the correct end, ahem. I followed the .plans logic, and always wondered why the base AI was just that; basic, and never got to be included within the Client/Server structure, as the .plan seemed to indicate.@Tim: does that snippet mean that you are indeed using AIClient, etal or is that method part of AIPlayer?? Or have you compiled some extraness into the stock AI provided??
Torque Owner Skylar Kelty
SkylarK