The first time we tried to implement this we made the torch radius configurable through the command console. Not so great for trying to setup a candle or a sunrod, while not affecting every other torch on the map. This time, we're going to add mouse selection and keyboard commands to modify individual torches.
BattleMapTorch.uc
The first thing we need is an object that looks like fire. I simply followed the 3D Buzz Video Tutorials on particle emitters. Literally, click-for-click. The only thing I did different was turn off the smoke, since it would obscure the map from our top-down view.
Once you have the emitter built, that becomes the base for our torch object. But, we need to add some features to it. Drop one onto a map, then open the properties window. There's a nifty little icon in the upper right hand corner that looks like a wrench. Choose "Copy To Clipboard", then paste the contents into an editor. What you see are the properties that would be set in the DefaultProperties section of an emitter.uc object. That's what we'll start with. (Not all of those properties are necessary, but it was certainly easier to start with that than trying to figure them all out from scratch.)
Next, we add some network replication functionality. The BattleMap does work with multiple clients, however we found that using InputDirector is easier. (See the post on the setup.)
Moving the torch is simply a matter of switching between two states: one that updates the torch's location from the mouse position and one that doesn't. We turn off collision detection when we're moving it around to avoid it getting stuck on walls or terrain. Why does stopping the torch also include updating the location? That has to do with network replication. For efficiency, synchronization information is not always transmitted. Some packets are skipped. (That's why you can be aiming right at someone's head but still miss a shot.) So, when the torch is no longer moving it may actually appear to be in different locations on clients. This forces every copy of the torch to be in exactly the same spot.
Lastly, we add a LightAttachment component to the torch to actually create the torch light. Otherwise, it would just be a flame in the dark.
class BattleMapTorch extends EmitterSpawnable;
var Color LightColor;
var float LightScale;
var PointLightComponent LightAttachment;
var Vector VerticalOffset;
var repnotify Vector TorchLoc,TorchStopLoc;
var float TorchRadius;
replication
{
if (bNetDirty)
TorchLoc,TorchStopLoc;
}
// called on clients when TorchStopLoc gets replicated
simulated event ReplicatedEvent( name VarName )
{
if (VarName == nameof(TorchLoc) )
SetLocation( TorchLoc );
else if (VarName == nameof(TorchStopLoc) )
StopTorch( TorchStopLoc );
else
super.ReplicatedEvent( VarName );
}
// this function should be run on both clients and server
simulated function MoveTorch( )
{
bCollideWorld = false;
SetCollision(false, false);
GotoState( 'TorchMoving' );
}
simulated function StopTorch( Vector NewTorchStopLoc )
{
bCollideWorld = true;
SetCollision(true, true);
GotoState('TorchStopped');
TorchStopLoc = NewTorchStopLoc;
SetLocation(NewTorchStopLoc);
}
simulated state TorchMoving
{
simulated event Tick( float DeltaTime )
{
// move the torch...
TorchLoc = BattleMapPlayerController(Owner).MouseOrigin + VerticalOffset;
SetLocation(TorchLoc);
}
}
simulated state TorchStopped
{
simulated event Tick( float DeltaTime )
{
// stop the torch...
}
}
simulated function PostBeginPlay()
{
LightAttachment = new(self)class'PointLightComponent';
LightAttachment.SetLightProperties(LightScale, LightColor);
LightAttachment.SetEnabled(true);
LightAttachment.Radius = TorchRadius;
LightAttachment.SetTranslation(vect(0, 0, 1) * 50);
if ( LightAttachment != None )
{
self.AttachComponent(LightAttachment);
}
super.PostbeginPlay();
}
DefaultProperties
{
TorchRadius=600
VerticalOffset=(X=0,Y=0,Z=50)
LightColor=(B=0,G=200,R=255,A=255)
LightScale=4.0
DrawScale=2.000000
Begin Object Name=ParticleSystemComponent0
Template=ParticleSystem'BattleMapAssets.Particles.PS_Torch'
ReplacementPrimitive=None
LightingChannels=(bInitialized=True,Dynamic=True)
SecondsBeforeInactive=0
End Object
ParticleSystemComponent=ParticleSystemComponent0
Begin Object Class=CylinderComponent Name=CollisionCylinder
CollisionRadius=+0064.000000
CollisionHeight=+0064.000000
BlockNonZeroExtent=true
BlockZeroExtent=true
BlockActors=true
CollideActors=true
End Object
CollisionComponent=CollisionCylinder
Components.Add(CollisionCylinder)
RemoteRole=ROLE_SimulatedProxy
bAlwaysRelevant=true
bReplicateMovement=true
bNetTemporary=false
bCanBeDamaged = false
bCollideActors = true
bBlockActors = true
bCollideWorld = true
bStatic=false
bNoDelete=false
bMovable=true
}
BattleMapPlayerInput.uc
Now, we need to add functions to our PlayerInput object to manipulate torches. First is a method to spawn a torch. We created an all-purpose spawn function, and will use keybind parameters to pass in what to spawn:
exec function BMSpawn(string ObjectToSpawn)Next, we want to activate functionality of an object when it's selected. In this case, we want the torch to start moving when it's clicked. But each object may do something different. For example, the triggers we implemented earlier will simply "turn on" -- nothing to continue doing. So, the function will first stop whatever a currently selected object may be doing (stop moving if we had already selected a torch) then start whatever the newly selected object needs to do. Replace the BMInteractObject() function with the following:
{
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
{
switch(ObjectToSpawn)
{
case "Torch":
Spawn(class'BattleMapTorch',Outer,,MouseOrigin,,,true );
break;
}
}
}
function string ParseObjectName(Name ObjectName)As we add more interactive objects to our BattleMap, we'll insert the method to trigger them in this function. Also, add this declaration to the top of the code along with GridZoomDelta:
{
local string ObjectType;
ObjectType = Repl(ObjectName, "BattleMap", "");
if (InStr(ObjectType, "_") > -1)
ObjectType = Mid(ObjectType, 0, InStr(ObjectType, "_"));
return ObjectType;
}
exec function BMInteractObject()
{
local BattleMapTorch To;
local Trigger Tr;
local int i, j, k;
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
{
// stop doing whatever the currently selected object is doing
if (ObjectSelected != none)
{
switch(ParseObjectName(ObjectSelected.Name))
{
case "Torch":
To = BattleMapTorch(ObjectSelected);
To.StopTorch(MouseOrigin);
ObjectSelected = none;
break;
}
}
//start doing something with a newly selected object
switch(ParseObjectName(ObjectUnderMouse))
{
case "Torch":
foreach DynamicActors(class'BattleMapTorch', To)
if (To.Name == ObjectUnderMouse)
{
ObjectSelected = To;
To.MoveTorch();
}
break;
case "Trigger":
// activate each Kismet event attached to this trigger
foreach DynamicActors(class'Trigger', Tr)
if (Tr.Name == ObjectUnderMouse)
for (i=0; i<Tr.GeneratedEvents.Length; i++)
for (j=0; j<Tr.GeneratedEvents[i].OutputLinks.Length; j++)
for (k=0; k<Tr.GeneratedEvents[i].OutputLinks[j].Links.Length; k++)
Tr.GeneratedEvents[i].OutputLinks[j].Links[k].LinkedOp.ForceActivateInput(Tr.GeneratedEvents[i].OutputLinks[j].Links[k].InputLinkIdx);
break;
}
}
}
var Object ObjectSelected;Finally, once we have a torch selected we have the ability to modify just that instance of the object. In this case, we want to be able to modify its light radius. Another frustrating issue we had recently, due to the summer time change, was seeing the projected map in a lit room. Even with the lights off, it can be difficult to see what you thought was a brightly lit map in the editor. So, we wanted the ability to adjust the map's brightness. Therefore if no object is selected, the default behavior will be to manipulate the lights in a map. Note that this only works for dynamic lights. Static lights are... static. Add the following function:
exec function BMModifyObject(int Modifier)DefaultInput.ini
{
local BattleMapTorch T;
local PointLightToggleable L;
local LightComponent C;
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
{
if (ObjectSelected != none)
{
switch(Left(ObjectSelected, 14))
{
case "BattleMapTorch":
T = BattleMapTorch(ObjectSelected);
T.LightAttachment.Radius = T.LightAttachment.Radius * (1 + (0.1 * Modifier));
break;
}
}
else
{
foreach AllActors(class'PointLightToggleable', L)
{
C = L.LightComponent;
C.SetLightProperties(C.Brightness * (1 + (0.25 * Modifier)));
}
}
}
}
All that's left is to bind keys to the functions we created. A "T" will spawn a torch. Left clicking will interact with objects. And the plus/minus keys ("Equals"/"Underscore") will manipulate selected objects. Replace the "I" binding in our BattleMap Bindings section with the following:
.Bindings=(Name="T",Command="BMSpawn Torch")
.Bindings=(Name="LeftMouseButton",Command="BMInteractObject")
.Bindings=(Name="Equals",Command="BMModifyObject 1")
.Bindings=(Name="Underscore",Command="BMModifyObject -1")
Here's what it looks like in action:
BattleMap mode source files