Friday, December 2

Unrealscript Overview Part Six : Actor

We don't actually place bubbles in the world. We use an object placed in the level as a point to spawn bubbles from, particle - system style. BubbleSpawner is this object. Note that it is marked as a placable so that it can be placed in the editor. The variables qualified by the (Bubble) suffix in their declaration appear as editiable properties in-game. This is an important mechanism to allow designers and artists to tweak the game without programmer input, letting you get on with core logic implementation.



In PostBeginPlay we vary the spawnCounter a bit, to be sure that mutliple emitters aren't in phase. Tick() just counts down using delta time, waiting until the time calsle to spawn a bubble. When it comes, the Spawn() function is called to randomly create a new BubblePawn somewhere randomly around the general vicinity of the spawner. That's all it takes to dynamically create a new in-world actor - the Pawn itself should take care of spawning it's controller.


class BubbleSpawner extends Actor placeable;

var(Bubble) float spawnRange;
var float spawnCountDown;
var(Bubble) float spawnInterval;

event PostBeginPlay()
{
spawnCountDown = spawnInterval * 2.5;
super.PostBeginPlay();
SetHidden(True);
}

event Tick(float DeltaTime)
{
local Vector SpawnLocation;
spawnCountDown -= DeltaTime;
if (spawnCountDown < 0)
{
SpawnLocation = self.Location;
SpawnLocation.X += -spawnRange + FRand() * (spawnRange * 2.0);
SpawnLocation.Y += -spawnRange + FRand() * (spawnRange * 2.0);
spawnCountDown = spawnInterval;
Spawn(class'LavaLamp.BubblePawn', Self,, SpawnLocation );
}
}

defaultproperties
{
spawnInterval = 0.75
spawnRange = 64.0
}

Unrealscript Overview Part Five : Player Controller and Camera

The PlayerController, derived from GamePlayerController, is the Player counterpart of the NPC GameAIController. The Player is put into the walking state by default. We co straight from here to the Spectating state, suitable for a free-flying camera. The states are implemented in GamePlayerController and it's base classes. The UpdateRotation() method is called from the base controllers Tick(). It's job is to transfer changes in PlayerInput and translate them into actual motion that sets the position and orientation of the player. ProcessMove() simply updates the Pawn's acceleration and leaves it to the base clases to update the Pawns velocity and position accordingly.

Player Controller


class LavaLampPlayerController extends GamePlayerController;

defaultproperties 
{
 CameraClass=class'LavaLamp.LavaLampPlayerCamera'
}


simulated event PostBeginPlay()
{
 super.PostBeginPlay();
}



/*
* The default state for the player controller
*/

state PlayerWalking
{
 event BeginState(Name PreviousStateName)
 {
  GotoState('Spectating');
 }
}


/*
* The default state for the lava lamp demo
*/

state PlayerSpectating
{
 event BeginState(Name PreviousStateName)
 {
  bCollideWorld = false;
 }

 function ProcessMove(float DeltaTime, vector NewAccel, eDoubleClickDir DoubleClickMove, rotator DeltaRot)
 {
  if( Pawn == None )
  {
   return;
  }

  if (Role == ROLE_Authority)
  {
   // Update ViewPitch for remote clients
   Pawn.SetRemoteViewPitch( Rotation.Pitch );
  }

  Pawn.Acceleration = NewAccel;
 }



 function UpdateRotation( float DeltaTime )
 {
  local Rotator   DeltaRot, newRotation, ViewRotation;

  ViewRotation = Rotation;
  if (Pawn!=none)
  {
   Pawn.SetDesiredRotation(ViewRotation);
  }


  // Calculate Delta to be applied on ViewRotation
  DeltaRot.Yaw      = PlayerInput.aTurn;
  DeltaRot.Pitch    = PlayerInput.aLookUp;

  ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );

  SetRotation(ViewRotation);
  NewRotation = ViewRotation;
  NewRotation.Roll = Rotation.Roll;
  if ( Pawn != None ) {
   Pawn.FaceRotation(NewRotation, deltatime);
  }
 }
}

Player Camera


class LavaLampPlayerCamera extends Camera;


function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
{
 local CameraActor   CamActor;
 local Pawn     TPawn;

 // Don't update outgoing viewtarget during an interpolation 

 if( PendingViewTarget.Target != None && OutVT == ViewTarget && BlendParams.bLockOutgoing )
 {
  return;
 }

 // Default FOV on viewtarget
 OutVT.POV.FOV = DefaultFOV;

 // Viewing through a camera actor.
 CamActor = CameraActor(OutVT.Target);

 if( CamActor != None )
 {
  CamActor.GetCameraView(DeltaTime, OutVT.POV);

  // Grab aspect ratio from the CameraActor.
  bConstrainAspectRatio   = bConstrainAspectRatio || CamActor.bConstrainAspectRatio;
  OutVT.AspectRatio   = CamActor.AspectRatio;

  // See if the CameraActor wants to override the PostProcess settings used.
  CamOverridePostProcessAlpha = CamActor.CamOverridePostProcessAlpha;
  CamPostProcessSettings = CamActor.CamOverridePostProcess;
 }
 else
 {
  TPawn = Pawn(OutVT.Target);

  // Give Pawn Viewtarget a chance to dictate the camera position.
  // If Pawn doesn't override the camera view, then we proceed with our own defaults
  if( TPawn == None || !TPawn.CalcCamera(DeltaTime, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV) )
  {   
   // for this demo, we just follow the player controller
   OutVT.POV.Rotation = PCOwner.Rotation;              
   OutVT.POV.Location = PCOwner.Location;
  }
 }


 // Apply camera modifiers at the end (view shakes for example)
 ApplyCameraModifiers(DeltaTime, OutVT.POV);
}


defaultproperties
{

}



Unrealscript Overview Part Four : Player Pawn

Our "Player" class is much less interesting than you might expect, as we are not implementing a player as such but a simple free camera. We override GetBaseAimRotation to control the viewpoint of the Pawn. We ensure that the Pawn can have it's view updated fully by the rotation pitch, roll and yaw angles. If we wanted to limit the viewing direction we would hard code tempRot.pitch = 0 in this routine to disable looking up and down for instance. BecomeViewTarget is called when a camera focuses on the player, and is our chance to block the request. Here, we allow it. As there is no mesh associated with our "Player" the light enviroment is redundant.

class LavaLampPawn extends GamePawn;



var DynamicLightEnvironmentComponent LightEnvironment;


//override to make player mesh visible by default

simulated event BecomeViewTarget( PlayerController PC )
{
 Super.BecomeViewTarget(PC);
}



simulated singular event Rotator GetBaseAimRotation()
{
 local Rotator tempRot;
 tempRot = Rotation;
 SetRotation(tempRot);
 return tempRot;
}



defaultproperties
{
 bCanWalk = true
 bCanSwim = false
 bCanFly  = false
 bCanClimbLadders = false
 bWorldGeometry = true
 bCollideActors = false
 bCollideWorld  = false
 bBlockActors   = false
 Begin Object Class=DynamicLightEnvironmentComponent Name=MyLightEnvironment
  bSynthesizeSHLight=TRUE
  bIsCharacterLightEnvironment=TRUE
  bUseBooleanEnvironmentShadowing=FALSE
 End Object

 Components.Add(MyLightEnvironment)
 LightEnvironment=MyLightEnvironment
}

Unrealscript Overview Part Three : NPC Controller

However, a pawn is not enough to give you a full NPC. A controller is also needed. Think of this as a model - view - controller set up. The view is handled by the rendering engine iteslf, the model is the unrealscript code for the pawn, and the actual controller is the controller class associated with the Pawn itself. Nearly all NPC or player pawns will have a controller associated with them. In the case of NPCs this will derive from AIController, which derives from GameController. These base classes define some logic for simple pathfollowing which may or may not be useful. In our example, the BubbleController does not use this derived class code.


AI Controllers usually are implemented as State machines. Unrealscript has a state construct that makes this easy. Code enclosed in a named state block is executed only when the object enters that state. The controller has an explicit method for switching state called GotoState(). As this is a simple example I have not overridden any methods inside the state block, but have written short fragments of code that can be executed latentley. Latent functions are Unrealscript speak for functions that run in the background. There are a small group of these available to you (grepping through the Controller, Actor and Pawn classes should reaval them), mostly related to Pawn movmement. Here, the rising state sets the Pawn velocity when it is etered, and then periodically checks to see if the bubble has risen too far. When it has, it enters the Popping state, wherein it stands still for a second, and then is destroyed.


The Destroy() function brings us to another point. Objects derived from actor have a special property in that they are spawned rather than created with the new() operator. In our BubblePawn class we called a base method called SpawnDefaultController(), and set ControllerClass to class'LavaLamp.BubbleController'. SpawnDefaultController will then create an instance of that controller class, and ensure that the controller Poseses (ie controls/owns) the Pawn by calling Posses. It is perfectly theoretially possible to pass a pawn around different controllers to implement different behaviours - although the state mechanism makes this less useful than it might sound.

class BubbleController extends AIController;



var float RisingDistance;

var float ExtraVelocity;



simulated Event SetInitialState() 

{

 ExtraVelocity = FRand() * 1.25;

 bScriptInitialized = true;

 GotoState('Rising');

}



state Rising

{

Begin: 

 Pawn.Velocity = vect(0.0, 0.0, 0.85);

 Pawn.Velocity.Z += ExtraVelocity;

RisingLoop:

 if (Pawn.Location.Z  > RisingDistance)  {

  StopLatentExecution();

  GotoState('Popping');

 }

 Sleep(1);

 goto 'RisingLoop';

}



state Popping

{

Begin:

 Pawn.Velocity = vect(0.0, 0.0, 0.0);

PoppingLoop:

 Sleep(1);

 Pawn.Destroy();

 Pawn = None;

 Destroy();

 goto 'PoppingLoop';

}





defaultproperties

{

 RisingDistance = 2048.0

}

Unrealscript Overview Part Two : NPC Pawn

The second class to look at is Pawn. The Pawn class extends the Object and Actor class and is responsible for most in-game entities. PostBeginPlay() is the method that is called when all engine-side initialisation of an actor is complete, so we usually do setup in here. In the case of the player, this means also spawning a default controller.


In BubblePawn, we are also initalising a material instance, an instance of a Material created with the Material Editor and given some named parameters within that editor. Creating a Material Instance lets us vary those parameters on a per-instance basis without creating a whole new material. This is what this looks like in the content browser.

..and in the Material Editor.



Tick() is the routine called every time the game state is updated. Overriding it enables you to completely alter the behavior of a given Pawn. In our case we are simply moving the bubble upward and varying it's scale to give a throbbing effect.


We are using dynamic lighting in our environment and thus we define properties for dynamic lighting of the object in our default properties. To aid speed we also turn off as many collision checks as possible. The static mesh component that we add is the mesh drawn by the renderer for this Pawn. The components are simply entries in a dynamic array of components and may be created or destroyed dynamically. The sprite removal is to simply remove a default placeholder sprite automatically added by the system, as this is a placeable and can actually be placed in the editor in the scene.






class BubblePawn extends Pawn placeable;



var float   accumulatedTime;

var StaticMeshComponent BubbleMesh;

var MaterialInstanceConstant MatInst;



simulated event PostBeginPlay()

{

 local LinearColor specularColor;



 accumulatedTime = FRand() * 180.0;

 super.PostBeginPlay();

 `log("spawning bubble ... ");

 SpawnDefaultController();

 MatInst = new(None) Class'MaterialInstanceConstant';

 MatInst.SetParent(BubbleMesh.GetMaterial(0)); 

 BubbleMesh.SetMaterial(0, MatInst);

 MatInst.SetScalarParameterValue('LavaEmissiveMultiplier', FRand());

 MatInst.SetScalarParameterValue('LavaSpecularPower', FRand() * 16.0); 

 specularColor = makeLinearColor(FRand(), FRand(), FRand(), 1.0);

 MatInst.SetVectorParameterValue('LavalSpecularColor', specularColor);

}



event Tick(float DeltaTime) 

{

 accumulatedTime += DeltaTime;

 SetDrawScale( 1.0 + 0.75 * Sin(accumulatedTime) );

 Move(self.Velocity);

 super.Tick(DeltaTime);

}





defaultproperties

{

 WalkingPhysics=Phys_NONE

 bCollideActors=false

 bCollideWorld=false

 DrawScale3D=(X=32.0,Y=32.0,Z=32.0)

 bNoEncroachCheck = true

 bIgnoreEncroachers = true

 ControllerClass=class'LavaLamp.BubbleController'

Begin Object Class=DynamicLightEnvironmentComponent Name=BubbleLightEnvironment

 ModShadowFadeoutTime=0.25

 MinTimeBetweenFullUpdates=0.2

 AmbientGlow=(R=.25,G=.25,B=.25,A=1)

 AmbientShadowColor=(R=0.15,G=0.15,B=0.15)

 LightShadowMode=LightShadow_ModulateBetter

 ShadowFilterQuality=SFQ_High

 bSynthesizeSHLight=TRUE

End Object

Components.Add(BubbleLightEnvironment)

Begin Object Class=StaticMeshComponent Name=InitialMesh

 StaticMesh=StaticMesh'Lava.Meshes.Bubble'

 bOwnerNoSee=false

 LightEnvironment=BubbleLightEnvironment;

 BlockRigidBody=false

 CollideActors=false

 BlockZeroExtent=false

 BlockNonZeroExtent=false

End Object

Components.Add(InitialMesh);

Components.Remove(Sprite);

BubbleMesh = InitialMesh

name='Bubble';

}

Dumping Blender Scenes as Lua


Following on to the previous post where I dumped blender scenes as Lisp, I've also written a modified version which is more usual for mainstream use that dumps the entire active object as an enormous Lua literal. I think this would be more useful for mainstream use as the Lua could then be compiled to binary and loaded into a tool, which could pick out and use the useful bits on a per-project basis. My next project is to investigate if such a thing is also possible with FBX & Python - again for more mainstream use.

Once more, here is the source Luke - use it :-)