Game Development Community

dev|Pro Game Development Curriculum

Stealth-game style Light-Level detection

by Ben Beshara · 04/09/2015 (8:38 am) · 11 comments

UPDATE: 29/4/2015
So I've made some advances to this resource and decided to rewriteit a bit to make the instructions a little clearer. The revised version is below.

I'm in the process of prototyping a game using the UAISK and Torque3D and stealth-like mechanics are central to my gameplay. After some searching and hacking together code I found there wasn't a simple solution to accurately measure the amount of light a player was subjected to using the rendered image (there may be a way to expose shadow volumes to script, but it was too elaborate for me to wrap my head around) so instead I decided to emulate the light volumes.

A video of the end result:

This resource requires a couple of simple engine changes in order to function well, but they can be skipped if you really don't want to alter the engine. We'll begin with those and as we go on, I'll note where it needs to be altered in script to negate the engine changes.

Engine Changes (optional):

Firstly, we're going to add a bool to light objects that will mark them as 'stealth-lights', so that we can pick and choose which lights will contribute to the light level calculations.

In T3D/lightBase.h add the following above "bool mIsEnabled":
bool mIsStealthLight;

Then in T3D/lightBase.cpp above 'mIsEnabled(true)':
mIsStealthLight( true ),

above the addField definition for isEnabled:
addField( "isStealthLight", TypeBool, Offset( mIsStealthLight, LightBase ), "Does this light contribute to the player light level used in stealth calculations?" );

above 'stream->writeFlag( mIsEnabled );'
stream->writeFlag( mIsStealthLight );

and above 'mIsEnabled = stream->readFlag();'
mIsStealthLight = stream->readFlag();

Now we'll have a nice flag on all our light objects that can be toggled in the world editor!

Next, we're going to expose the brightness level of lights accoridng to their animation state. This is so that if we place animated lights in the scene, we will have a brightness value that adjusts to the brightness of the light in its current frame of animation. This just allows us to be more crafty with our stealth designs; flickering lights that provide only moments of visibility to the AI, or pulsing lights players have to time their way past.

In T3D/lightAnimData.h add the following at about line 110:
F32 getBrightness( LightAnimState *state );

Then in T3D/lightAnimData.cpp at the very end of the file:
F32 LightAnimData::getBrightness( LightAnimState *state )
{
	//PROFILE_SCOPE( LightAnimData_animate );
	F32 time =  state->animationPhase + 
               ( (F32)Sim::getCurrentTime() * 0.001f ) / 
               state->animationPeriod;

	F32 brightness = state->brightness;
	mBrightness.animate( time, &brightness );
	return( brightness );
}

In T3D/lightBase.h around line 136:
F32 getTrueBrightness( void );

And finally at the end of T3D/lightBase.cpp:
DefineConsoleMethod( LightBase, getTrueBrightness, F32, (), , "Returns the brightness level of the light, taking into account its current animation state." )
{
    return( object->getTrueBrightness() );
}

F32 LightBase::getTrueBrightness( void )
{
    if ( mAnimState.active && mAnimationData )
    {
        return( mAnimationData->getBrightness( &mAnimState ) );
    }
	else
	{
		 return( mBrightness );
	}
}

Now we have a function, getTrueBrightness(), that when called on a light object will return the brightness of the current animation frame.

And that's it for the engine changes. Recompile and your lights will have an isStealthLight flag and a getTrueBrightness function, which we will be using shortly.

Script Additions:

Firstly, at the end of my GameConnection::onClientEnterGame() function I've created a ScriptTickObject. I originally used a schedule timer to perform the light checks, but found that the effect it produced wasn't as fluid as I would have liked. In previous versions of Torque the only way to have code execute between ticks (such as a moving object) was to use C++. It worked, but wasn't friendly to quick-and-dirty prototyping. ScriptTickObject rectifies this and from my limited experience, seems to work very well.

So, at the bottom of GameConnection::onClientEnterGame():
//Set up the tick object to perform stealth checks
   if(!isObject(stealthTicker))
   {
   	%tickObj = new ScriptTickObject(stealthTicker);
   	%tickObj.setProcessTicks(true);

	  MissionCleanup.add(%tickObj);
   }
(adapted lovingly from the kind Mr. Martin here)

Then I created a new file, but I suppose, really, it can go anywhere you please. This is the function which performs the light checks. Firstly, we reset the %currLightLevel var. Then we get the player object and start a ContainerRadiusSearch for light objects. If it is a stealth-light, we grab its range (if it's a directional light) or radius (if it's an omni light) and see if the player is within its reach.

If it is an omni light, a raycast is performed between the light and the player to see if the view between the player and light is obstructed. If it is a directional light (watch out for my hacky bad vector maths - please, if you can improve this at all let me know!) an array of raycasts is performed in the range of the light to see if the player is within its reach. This is far from a perfect solution, but it mostly works. If you want to visualise the raycasts, uncomment the DebugDraw call.

If the view between the light and the player is unobstructed, the distance between the light and the player has some crafty maths applied to it and is then assigned to the player object, and a command sent to the client to update their stealth meter. The value is a float between 0 and 1, so I'm sure you can come up with something cool to represent it.

function light_test(){
   %currLightLevel = 0;
   if(%clientConn = clientGroup.getObject(0)){ 
      //Get the player's position
      %player = %clientConn.getControlObject(); 
      %camPos = %player.getPosition();
   
      //Set a search radius - vary this on difficuty?
      %searchRadius = 30;
   
      //Do a radius search for light objects
      initContainerRadiusSearch(%camPos, %searchRadius, $TypeMasks::LightObjectType, false);
   
      //See if we found anything
      while((%light = containerSearchNext()) != 0){
         
         //If we did, we check to see if this light has checks enabled
         //If you did't make the engine changes, comment out the below line and uncomment the one below that.
         if(%light.IsStealthLight && %light.IsEnabled){
         //if(%light.IsEnabled){
         
            //Now check to see if we're in range of light, and by how much
            if(%light.radius){
               %lightSize = %light.radius;
            } else if(%light.range) {
               %lightSize = 0;
               %addVec = VectorScale(%light.getForwardVector(), %light.range);
               %newPos = VectorAdd(%light.getPosition(), %addVec);
               %vecTypemasks = $TypeMasks::StaticObjectType | $TypeMasks::ShapeBaseObjectType;
               
               %lightSpread = mTan(mDegToRad(%light.outerAngle / 2)) * %light.range;
               
               //Centre Vector
               %vecList[0] = %newPos;
               
               //Outside Vectors
               %vecList[1] = VectorAdd(%newPos, "0 0 " @ %lightSpread);
               %vecList[2] = VectorAdd(%newPos, "0 0 -" @ %lightSpread);
               %vecList[3] = VectorAdd(%newPos, "0 " @ %lightSpread @ " 0");
               %vecList[4] = VectorAdd(%newPos, "0 -" @ %lightSpread @ " 0");
               %vecList[5] = VectorAdd(%newPos, "0 " @ (%lightSpread / 1.5) @ " " @ (%lightSpread / 1.5));
               %vecList[6] = VectorAdd(%newPos, "0 " @ (%lightSpread / 1.5) @ " -" @ (%lightSpread / 1.5));
               %vecList[7] = VectorAdd(%newPos, "0 -" @ (%lightSpread / 1.5) @ " -" @ (%lightSpread / 1.5));
               %vecList[8] = VectorAdd(%newPos, "0 -" @ (%lightSpread / 1.5) @ " " @ (%lightSpread / 1.5));
               
               //Inside Vectors
               %vecList[9] = VectorAdd(%newPos, "0 0 " @ (%lightSpread / 2));
               %vecList[10] = VectorAdd(%newPos, "0 0 -" @ (%lightSpread / 2));
               %vecList[11] = VectorAdd(%newPos, "0 " @ (%lightSpread / 2) @ " 0");
               %vecList[12] = VectorAdd(%newPos, "0 -" @ (%lightSpread / 2) @ " 0");
               %vecList[13] = VectorAdd(%newPos, "0 " @ (%lightSpread / 3) @ " " @ (%lightSpread / 3));
               %vecList[14] = VectorAdd(%newPos, "0 " @ (%lightSpread / 3) @ " -" @ (%lightSpread / 3));
               %vecList[15] = VectorAdd(%newPos, "0 -" @ (%lightSpread / 3) @ " -" @ (%lightSpread / 3));
               %vecList[16] = VectorAdd(%newPos, "0 -" @ (%lightSpread / 3) @ " " @ (%lightSpread / 3));
               
               %vecCount = 0;
               %tmpLightSize = 0;
               for(%vecCount = 0; %vecCount <= 17; %vecCount++)
               {
                  %vecCheck = containerRayCast(%light.getPosition(), %vecList[%vecCount], %vecTypemasks);
                  //DebugDraw.drawLine(%light.getPosition(), %vecList[%vecCount]);
                  
                  %obj = getWords(%vecCheck, 0, 0);
                  if(%obj){
                     if(%obj.getClassName() $= "Player"){
                        %lightSize = %light.range;
                     }
                  }  
               }
            } else {
               //We don't know how far this light shines, so let's ignore it
               %lightSize = 0;
            }

            if(containerSearchCurrDist() < %lightSize){
               //Now check to see if anything is blocking our line of sight.
               //If it is, we're in shadow and can ignore the light
               %losTypemasks = $TypeMasks::StaticObjectType | $TypeMasks::ShapeBaseObjectType;
               %losCheck = containerRayCast(%camPos, %light.getPosition(), %losTypemasks, %player);
            
               if(!%losCheck){
                  //If you did't make the engine changes, comment out the below line and uncomment the one below that.
                  %lightAmount = (1 - (1 / %lightSize * containerSearchCurrDist())) * %light.getTrueBrightness(); //mRound((1 - (1 / %lightSize * containerSearchCurrDist())) * 10);
                  //%lightAmount = (1 - (1 / %lightSize * containerSearchCurrDist())) * %light.brightness; //mRound((1 - (1 / %lightSize * containerSearchCurrDist())) * 10);
                  if(%lightAmount > %currLightLevel){
                     %currLightLevel = %lightAmount;
                  }
               }
            }
         }
      }
      //Send the light level to the player
      %player.lightLevel = mRound(%currLightLevel * 10);
      commandToClient(%clientConn, 'setLightMeter', %currLightLevel);
   }
}

Then we set our ticker to run a light_test();
function stealthTicker::onInterpolateTick(%this, %timeDelta){
   light_test();
}

And add a function on the client to receive the server's command:
function clientCmdSetLightMeter(%val)
{
   lightMeter.setValue(%val);
}

And that's it! This has been a great exercise to get me back into game programming after a hiatus and I hope some of you find this useful in your own projects.

Thanks,
Ben

About the author

I've been a Torque user since 2004 and as a previous user have worked with 1.3, 1.4, the TLK and TGEA ('TAT' lmao). I've come back under a fresh account to distance myself from my youthful history.


#1
04/10/2015 (9:41 pm)
Freaking awesome work! I had dome something similar for the AI, but the one I made only determined if there was a light source near instead of the amount of light. This is much better and detailed, thank you for sharing.
#2
04/10/2015 (11:58 pm)
Thanks :) There could be much more detail in this as well - I just realised that it doesn't take the light's brightness into account *at all* which was a bit of an oversight. If you change the line
%lightAmount = (1 - (1 / %lightSize * containerSearchCurrDist()));
to
%lightAmount = (1 - (1 / %lightSize * containerSearchCurrDist())) * %light.brightness;
the light values seem to better represent the actual volume of light.

I'm in a rush but I'll edit this into the resource when I have a spare few minutes!

UPDATE: I've made the changes above to the resource
#3
04/14/2015 (4:55 am)
ANOTHER UPDATE: There was little oversight (again) - the script didn't check to see if the light was enabled, I've now updated it to correct this
#4
04/15/2015 (3:32 pm)
Ok, how do I setup my lights??? I keep getting this error:

Unable to find object: 'lightMeter' attempting to call function 'setValue'

however I am getting this in console so my script is working;

Found: light
Distance: 50.0408

is it because I don't have anything like a progress bar hooked to it and if you could please explain how you did that to, thank you in advance for any help....
#5
04/19/2015 (12:05 am)
Hi Donnie - sorry for being so late to get back to you!
You're absolutely right - the easiest way to incorporate it is to just add a progress bar named 'lightMeter' to the PlayGUI; however if you don't want to display the light level to the player, you can simply comment out
commandToClient(%clientConn, 'setLightMeter', %currLightLevel);
at the end of the light_test() function. The value returned is a float between 0 and 1, so it should be trivial to find other ways to convey the light level to the player.

I've been putting some more work into this and getting directional lights working (somewhat), my vector and trigonometric maths skills have suffered in the last few years - I've also been working on using player's sound level (but it's integrated mostly with the UAISK, I'll see if I can abstract it out a little more). When I find time I'll update this resource accordingly.

I hope I've helped!
#6
04/19/2015 (12:06 am)
Also, if you haven't added the engine code in the post, you'll need to add a 'stealthLight = true;' field to the lights that will contribute to the player's light level.
#7
04/24/2015 (11:33 am)
Ok thanks ben for the great resource...
#8
04/29/2015 (4:23 am)
I've just updated the resource with all the changes, including directional light and animation support
#9
05/26/2015 (9:48 pm)
Hey ben, any chance you will share howcyou tied this in with uaisk, i have the same kit??
#10
05/27/2015 (8:07 am)
Sure thing :)

In aiGlobals.cs I've added the following variable:

// AI Light-base Stealth
$AISK_LIGHT_THRESHOLD = 0.5;


And in aiTargeting.cs I added this to the beginning of AIPlayer::IsTargetInView():

if(%obj.lightthresh){
  %light_thresh = %obj.lightthresh;
} else {
  %light_thresh = $AISK_LIGHT_THRESHOLD;
}

And wrapped the original content of the function (from the //No need to check comment to the return false;) inside of:

if(%tgt.lightlevel > %light_thresh){
...
} else return false;

(I would just post the whole function but I'm not sure how Twisted Jenius would feel about that/if he still lurks here/cares/is alive)

The AIPlayer the only registers targets whose light level is higher than their light threshold. This can be set manually or somehow tied into the bots alert level probably.

I hope this helps!
#11
06/12/2015 (7:23 pm)
Thanks ben great resource!!!