Game Development Community

Requesting some clarification on Datablocks

by Jason Gossiaux · in Torque Game Engine · 04/26/2008 (10:51 pm) · 10 replies

I've become rather familiar with editing the AIPlayer scripts and source where necessary to get my bots doing what I want. So with that out of the way I'd moved on to trying to get some AI driven towers into my game. This has proven.. most challenging :P

I wanted a stripped out turret so I based it off a static shape and not an aiplayer. It doesn't need to move, or aim (towers will be entirely stationary) but possibly animate which I believe static shapes can do. I also needed to divorce myself from the PLAYERDATA implementation of a fixed rectangular bounding box.

So my datablock is as follows:

datablock StaticShapeData(BlueTower)
{
	shapeFile = "~/data/models/towers/Blue_Tower.dts";
	category = "Towers";
	shadowCanMove = true;
	shadowCanAnimate = true;
	maxDamage = 50;
};

Then I spawn them via an advanced placement mechanic..

function AITower::spawnAtLocation(%Name, %spawnPoint, %type)
{
   if(%type == $BUILD_BlueTower) 
  {
		   %tower = new AiTower() 
		   {
				dataBlock = BlueTower;
		   };
	}
	else if(%type == $BUILD_PurpleTower)
	{
		   %tower = new AiTower() 
		   {
				dataBlock = PurpleTower;
		   };		
	}
   MissionCleanup.add(%tower);
   %tower.setTransform(%spawnPoint);
   return %tower;
}

And all this works fine. However trying to tie damage and death into them has given me some issues. I looked at how the player tied into the shapebase code, and tried modeling it in the source. So in the source I added a new AITower.cc file that includes the following:

#include "game/aitower.h"
#include "console/consoleInternal.h"
#include "core/realComp.h"
#include "math/mMatrix.h"
#include "game/moveManager.h"

IMPLEMENT_CO_NETOBJECT_V1(AITower);

/**
 * Constructor
 */
AITower::AITower() : StaticShape() // modified by JDG for movement speed
{
   mTypeMask |= AIObjectType;
}

/**
 * Destructor
 */
AITower::~AITower()
{
}

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

const char* AITower::getStateName()
{
   if (mDamageState != Enabled)
      return "Dead";

   return "Move";
}

void AITower::throwCallback( const char *name )
{
   Con::executef(getDataBlock(), 2, name, scriptThis());
}

This was necessary so the script code would work. Now when I shoot the tower with my crossbow, the Damage function in script gets called. However the function in script is different from what is used in the Player script.

function AITower::Damage(%this, %sourceObject, %position, %damage, %damageType)
{

vs

function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{

Why does the Armor damage have %this AND %obj? I am semi-following the exploding barrel resource from the Advanced 3d Game Programming All in One resource and it also has the %this and %obj. Do I need to add something to the source to link this properly?

It isn't really a problem I don't think? I mean, Damage gets called and ApplyDamage works as expected. I just feel like I hacked this together through trial and error and wish I understood it better.

Furthermore I am finding that setting variables in the datablock isn't working. maxDamage as applied in the datablocks above never works. If I go into the console and do $TowerID.maxDamage = 100; then it works. But doing it in the datablock in script doesn't. I can add it to the OnAdd I guess, but why?

Please let me know if you need any more information. My goal was to create an object that doesn't have as much of the overhead as the Player/AIplayer, while still allowing for Placement, Collision, Damage, Death, and for it to think and Shoot back. Thanks in advance for any help you can give me!

#1
04/26/2008 (11:40 pm)
You are missing some key C++ methods for your datablock class:

--packUpdate/unPackUpdate()
--initPersistFields()

On the callback code (Con::executef(...)), what is important to recognize is that the callback happens on the datablock of the tower, not the tower itself. You can tell this because of the way you have the callback configured in c++:

Con::executef(getDataBlock(), 2, name, scriptThis());

Note that getDataBlock() is the first argument, which means that the callback will be executed on the namespace of the object's datablock, not the object itself. In your case, it should look like:

BlueTower::xxxx(%this, ...)

You do however have several errors in how you have the Con::executef(...) itself set up--for one, you aren't providing a method to call back on within the namespace (at least, not with the common technique of a hard coded method name in quotes). Right now, the callback is being executed on the method controlled by the contents of the class variable name, whatever that holds.

You can find a pretty good reference on the various things you need for accomplishing what you want in the General Documentation for the Console, and some searches for Con::executef in the code as well as the forums should help out a lot :)
#2
04/27/2008 (12:14 am)
Hmm, well I'm not trying to create a new datablock from scratch. I was trying to do what was done with AIPlayer. Except I was building it atop StaticShapeData instead of onto of PlayerData.

The InitPersistFields occurs in the ShapeBaseData and StaticShapeData classes, with the damage levels being defined there. So I didn't think I had to redefine them much like AIPlayer doesn't. Of course if I add any new variables I'd have to.

The Con::executef code was copied from the AIPlayer.cc file as well. I basically was trying to clone AIPlayer.cc except for a StaticShapeData and minus all the ConsoleMethods and other functions I didn't need. I hope that makes sense.
#3
04/27/2008 (12:20 am)
Let me clarify a bit more about how I was using AIPlayer...maybe I wasn't doing things correctly there either.

So in the AIplayer.cs file I have

datablock PlayerData(GreenBot : PlayerBody)
{
	CurrentAction = $A_Idle;   // The bot's current action
	CurrentPathID = 0;         // The ID of the path the bot is currently walking
	CurrentMoveObjectID = 0;   // The ID of the object the bot is walking to
	CurrentPathCounter = 0;    // A counter 
	CurrentTargetLocation = 0; // The Location the bot is currently walking towards
	CurrentManagedNode = 0;    // The current node the bot is walking to, and therfore has blocked for others to walk to
	CurrentSelfBlockedNode = 0;// The bot's current non-moving location.  Used so other bots can walk around him more easily.
	IsStuck = 0;				// this flag is used so the bot can take a step backwards and try to get unstuck.
	StuckNode = "";            // This lets the stuck bot pathfind around the stuck area if possible.  
	shapeFile = "~/data/models/boxman_green/boxman_green.dts";
};

datablock PlayerData(YellowBot : PlayerBody)
{
	CurrentAction = $A_Idle;
	CurrentPathID = 0;
	CurrentMoveObjectID = 0;
	CurrentPathCounter = 0;
	CurrentTargetLocation = 0;
	CurrentManagedNode = 0; 
	CurrentSelfBlockedNode = 0;
	IsStuck = 0;				// this flag is used so the bot can take a step backwards and try to get unstuck.
	StuckNode = "";            // This lets the stuck bot pathfind around the stuck area if possible.  
	shapeFile = "~/data/models/boxman_yellow/boxman_yellow.dts";
};

function AIPlayer::spawn(%name,%spawnPoint, %type)
{
   // Create the demo player object
   
   if(%type == 1) 
   {
		   %player = new AiPlayer() 
		   {
				dataBlock = GreenBot;
				path = "";
		   };
	}
	else if(%type == 2)
	{
		   %player = new AiPlayer() 
		   {
				dataBlock = YellowBot;
				path = "";
		   };		
	}
   

   MissionCleanup.add(%player);
   %player.setShapeName(%name);
   %player.setTransform(%spawnPoint);
   return %player;
}

So I did this to have flavors of AIPlayer each with their own models. This has worked out well so far and Ive not had any problems with it. It also didn't require me to change the source of AIplayer.cc. So that is why I declared BlueTower and PurpleTower in the way I did.



UPDATE :

Well this was silly. I was forgetting that my script function should have been StaticShapeData::OnDamage not AITower::OnDamage. That is why one needs the %obj and one does not. This is similar to how the AIPlayer script file has several PlayerData:: functions at the top. It is really difficult keeping track of what datablock you are actually referencing, especially when going back between the source and scripts.

In any case, Stephen your reply led to me re-evaluating how I was doing things and I answered my own question haha. Thanks!
#4
04/27/2008 (1:19 am)
Very glad that you got it figured out, and that it makes sense now :) It's really important to understand the low level details of datablocks, callbacks, and namespaces, and not something that is easily explained or understood.

The choice of when to issue a callback on an object vs a callback on an object's datablock can be a tricky one, so I'll throw out my usual explanation case for follow-on readers because it normally tends to make the most sense to the most people.

Imagine for a moment that you have a game with three different types of players:

Robot: moves slowly, but has same movement speed on ground, and in water. Is immune to drowning.

Soldier: moves very fast on ground, but slowly underwater, drowns quickly.

Scuba Diver: moves slowly on ground (those fins are hard to walk in!), but swims very fast. Is immune to drowning (scuba tank).

Now, we could create completely new C++ classes for each of these "roles", and re-implement the physics for each, effectively having a RobotPlayer.cc, SoldierPlayer.cc, and ScubaDiverPlayer.cc, each inheriting from Player. However, if we plan things out well, we can have one Player class that takes into account values provided by datablocks (such as mDataBlock->maxForwardGroundSpeed, mDataBlock->maxForwardUnderWaterSpeed, etc.), and then write one set of physics to handle the forward movement based on those datablock values.

That works well for our movement requirements, but doesn't handle our "breathing underwater" case...or at least if we do do that in C++, we would have to create code that couldn't be changed via script easily.

Here's where datablock callbacks come in, and can be very powerful. The Player class developer (C++ programmer) could have chosen to have the callback to script occur on the Player class, and therefore scripters would write a callback handler that looked something like this:

Player::onEnterLiquid(%this, %coverage)
{
  if (%this.playerType $= "RobotPlayer")
  {
     // do Robot under water stuff
  }
  else if (%this.playerType $= "SoldierPlayer")
  {
    // do soldier underwater stuff
  }
   else (etc, etc.)
}

Would this work? Yes, pretty much. However, it's not really good programming practice, because every time we add a new "type" of player, we would have to go back and change code (TorqueScript in this case) to implement the new player type, which has the chance of breaking our implementation for other player types.

It's much better programming wise to instead issue the callback on the datablock of the object, instead of the class of the object, because now we can do this:

RobotPlayerData::onEnterLiquid(%datablock, %object, %coverage)
{
  // do robot specific under water stuff
}

SoldierData::onEnterLiquid(%datablock, %object, %coverage)
{
  // soldier stuff for under water
}

ScubaData::onEnterLiquid(%datablock, %object, %coverage)
{
  // ...
}

As should be pretty obvious now, I can freely change how the scuba type player reacts to entering water (maybe add a timer to how long they can stay under water before their air runs out) without touching the other types of players at all.

However, it's important here to realize that datablocks are possibly shared by many objects--so if we issue a callback on a datablock's namespace, we need to provide a reference to the specific player that the callback is associated with--which is why we have the new parameter "%object" in the datablock versions of the callback handlers. A more complete example of this particular callback would probably look like:

(note, this is a hypothetical example, I'm making it all up so functions and such don't actually exist)

SoldierData::onEnterLiquid(%datablock, %object, %coverage)
{
   if (%coverage > 0.9)
   {
      // head is under water, take damage
      %object.damageOverTime(%datablock.drowningDamage, %datablock.needAirRate);
   }
}

While made up, this second example shows where the %datablock reference can be useful, and also why the %object reference is critical--we don't want every Soldier player in the game taking damage when just one of them goes under water!
#5
04/27/2008 (11:32 am)
On a slightly unrelated node, you seem to be shoving everything and sundry into your datablocks. Things like IsStuck, if I'm reading you right, should NOT be per-datablock, but per-object. The idea with datablocks is that they're blueprints - they store things that every single object of one type has in common. So if one YellowBot sets %dataBlock.IsStuck to true, then all players using the YellowBot datablock will act like they're stuck.
#6
04/27/2008 (10:12 pm)
On a slightly unrelated node, you seem to be shoving everything and sundry into your datablocks.

That is just for initialization. When a bot spawns I want IsStuck to be 0. Each bot has a scheduled :
AIPlayer::think(%this)
{
    if(something to tell if I am stuck)
          %this.IsStuck = 1;

    %this.schedule(500,think);

}

With the line limit and length of my scripts it is hard to show it, but I only initialize some variables in the datablock, and then go ahead and access them on an object by object basis later. I hope that makes sense.
#7
04/28/2008 (12:37 pm)
Okay, that's cool. Just making sure, you know? No offence (or patronisation :P) was intended.
#8
04/28/2008 (1:47 pm)
It still won't work that way.

You're keeping currentPath that the bot is walking in your datablock. No matter how you twist it, it won't work with multiple bots on different paths, that way - and you're not using datablocks for what they were intended to.
#9
04/28/2008 (4:11 pm)
Hmm, ok I think I'm getting it. So what I did was set those variables for the one singular datablock that exists in the game. Not on each object. Later when I set %this.IsStuck = 1 I'm setting a DIFFERENT, object based IsStuck to 1, not the one defined in the datablock. Gotchya I think.
#10
04/28/2008 (4:59 pm)
You're on the right track. Think of a datablock as a blueprint, just like Daniel said above. For instance, a car is built around a blueprint, and if you produce two cars they will have the same properties that are defined within that blueprint.

Two cars of the same type would share the same *maximum* speed, size, weight etc. This is ideal properties for a datablock, because they rarely change. On the other hand, durability, fuel level and *current* speed is situational to each car and can't be shared, so those need to be kept on the object instead.

This is to save bandwidth when telling a client that multiple objects share the same properties, instead of having to describe each object eventhough they are very much alike, which would be wasted bandwidth. I'm sure there are also other reasons, but that's the main thing.

No matter what the car "does" - the blueprint never changes. The object does, however.