The problem is caused by the isometric camera perspective. A square drawn on the UI is actually trapezoidal in world space. You can't rely on just two points. You must pass all four UI positions to LineTraceByChannel to get the world position of each corner, which then represent the four planes of the trapezoid:
So how do you find the vehicles inside a polygon without a "PolygonOverlapActors"? There's a simple trick. Trace a ray outward from each vehicle and count the number of polygons it intersects using LinePlaneIntersection. If it intersects two or none, it's outside. If it only intersects one, it's inside:
I just added lasers to the vehicle weapon list.
We can now control a squad:
Next, we'll get them to be aware of each other and try to avoid colliding. (And yes, it's almost time for guns!)
This is the view from the NPC. Those would all be mine.
Parking in a specified direction involves two steps: the approach and the landing.
Parking, the Approach
We could just fling our vehicle directly at a destination and let it line itself up when it gets there. However since there are 360 angles it could arrive at and 360 angles it could be destined for, it may spend quite some time turning around. So ideally we want it as close as possible to its final rotation when it begins the landing.
To do that, we're going to inject a waypoint into the array of Path Points returned by the NavigationPath object. We want to calculate a waypoint that is 90 to 180 degrees behind the destination. If the vehicle is in front of the destination, it'll aim for 90 degrees to the side. As the vehicle drives behind the destination, it will curve inward to 180 degrees directly behind the destination:
The similarity to aircraft landing is not lost on me.
The distance behind the destination (the yellow dotted line above) is determined by:
I chose 1500 for maxDistance, as longer distances makes the vehicle appear to veer way off course and shorter distances didn't give it enough length to line up behind it.
The direction from the destination to insert the waypoint (the orange dotted line above) is:
The result is a rotator from 90 to 180 degrees, to be added to the final rotation. The numbers work out like this:
Angle
Rotator
Length
-180
90.1
1
-150
115.8
0.96
-120
137.9
0.86
-90
155.9
0.70
-60
169.1
0.5
-30
177.2
0.25
0
0
0
30
-177.2
0.25
60
-169.1
0.5
90
-155.9
0.70
120
-137.9
0.86
150
-115.8
0.96
180
-90.1
1
Here's the blueprint:
Parking, the Landing
Once the vehicle is likely behind the destination, probably facing nearly the correct direction, and within CloseEnough(), we can start actually parking.
The primary goal is to align the vehicle along the rotation plane that passes through the destination. That's the hard part. Then it's a simple matter to forward or reverse to the destination.
We need to calculate a target to steer toward again, but unlike the Approach target, the Landing target will always be in a line along the rotation plane. The farther to the side the vehicle is, the straighter it will aim directly for the plane. As it gets closer, it will steer to drive alongside it:
I would have been more interested in trigonometry if the problems were like this instead of the height of trees.
We need three values: how far the vehicle is along the plane, its distance away from the plane and whether the vehicle is facing toward or away from it.
We know how far the vehicle is along the plane (the yellow line above) using:
That results in a 1 or -1 describing whether the vehicle is aiming left or right of the destination and which side of the plane it's on. That becomes the vehicle's throttle.
Finally, we can calculate an exponentially increasing length away from the vehicle's position along the plane:
After the previous post, we now have rudimentary MoveTo functionality for WheeledVehicle pawns. However, locking the throttle axis at 1.0 causes some obvious problems steering around corners.
Before we tackle that, let's at least stop the vehicle when it reaches its destination.
Close Enough
I added a CloseEnough float variable to the unit, then compared it to the distance to the last NavPoint[] inside a CalculateThrottle() function. If it's close enough, we'll set the vehicle's state to "Parking".
Yes, state machines are out of fashion and behavior trees have obvious advantages. At first, I did build quite a bit of control logic inside the behavior tree. Yet as I continuously added variables to the Blackboard I realized I would then need multiple behavior trees: one for each type of unit. A helicopter would need vastly different controls. I would have duplicate trees (or extensive Switches) despite having very similar behaviors.
I think there's still room for both, and a mix of the two is needed here. The Behavior tree will generally define what the unit should be doing (navigating, following, attacking...) while the unit itself implements how to do it -- including how to stop doing it.
So in this case, the Behavior Tree asks "Am I Driving?" (Driving is the state set when MoveTo is called.) If so, find a path. The vehicle itself determines how it gets there. When the answer is no, the Behavior Tree will "idle" until it's given a new direction. When Parking, the vehicle sets the throttle to 0 and enables the handbrake.
Improved Throttling
At first, I approached cornering as a math problem and tried physics calculations to determine control settings around a corner. After numerous failures, it struck me that I don't calculate any of that in my head when I drive my own car.
So I enabled possession of a unit and had it drop pylons every time I changed a control.
Also recording steering: Not as useful
While I always steer toward my next immediate goal, throttle is determined by a future guess as to where the vehicle be and what direction it will be facing. Despite steering straight for the corner, I stop accelerating to prepare for the turn. Once in the turn, if I'm going too fast and not facing my next goal then I apply the handbrake and throttle to force a tighter turn.
I chose to predict the position of the car after one second, purely because it was easy to use GetForwardSpeed() and apply that to the forward vector.
Armed with those few rules, the logic is actually quite simple:
If the next NavPoint past the predicted distance is NavPoint[1], throttle = 1
If steering is straight but the angle to the predicted NavPoint is increasing, throttle = 0
If steering is not straight and angle is greater than 90, apply handbrake and throttle = 1
Navigation has been recalculated, it has a straight line to the first point NavPoint[1],
throttle = 1
Will it beat the player in a speed race? Probably not. Is it sufficient to simulate fearless post-apocalyptic survivors? Surprisingly so:
The logic now looks like:
Spline Paths
You'll notice that the rendered path (the green lines) expresses curves because I'm using a SplineComponent to connect the NavPoints. Let me save you some trouble lest you attempt to guide the vehicle with a spline or use it to predict the vehicle's position.
You can't specify the rotation of individual SplineComponent points. If you watch carefully in the videos or screenshots, you'll sometimes see exaggerated curves between some points as their rotations do not necessarily aim toward each other.
While the SplineComponent provides a very handy GetLocationAtDistanceAlongSpline() function, there is a significant amount of bookkeeping required to calculate that effectively as well as the curves mentioned above distorting the results. I found it much simpler to sum up the NavPoint segment lengths and check if my predicted distance is greater.
Next, we'll improve parking so that the vehicle stops at its destination facing a specified direction.
As a fan of Car Wars, The Road Warrior, Spy Hunter and Death Race, I want to arm and armor a squad of post-apocalyptic muscle cars then send them into the wasteland to challenge gangs of jury-rigged monstrosities-on-wheels fighting over gasoline and spare parts.
Engines like Unreal (and Unity) are available for free and it's easier than ever for inexperienced developers to create games. With no eagerly anticipated announcements forthcoming, like any smug mature gamer with a programming background I thought, "How hard could it be?"
As we're going to find out, fairly hard.
"Simple" Vehicle Movement
The first feature to tackle is point-and-click movement. Instead of directly controlling a single vehicle, we want a squad of vehicles to drive themselves to a specified destination.
I assumed it would be as simple as importing a vehicle from one of the examples, build a Behavior Tree Task and send it a MoveTo.
Done. Next on the list? Guns!
Except that pawns using the WheeledVehicleMovement component don't respond to MoveTo. They respond to Throttle, Steering and Handbrake. So not only do we have to tell it where to go, but how to get there.
The typical approach to racing game AI is to pre-build waypoints into a circular track. That won't work for this game due to the open-world style maps. We need to tell a vehicle to move from anywhere to anywhere. Fortunately, we can still use Unreal's NavMesh intended for character pawns. The FindPathToLocationSynchronously function simply needs Start and End vectors. The result is a NavigationPath object, which includes an array of Path Point vectors.
Who says A* Pathfinding is complicated?
Now we can build a short Behavior Tree Service that recalculates a path from the vehicle's location to its destination. Finally, have the vehicle always steer toward the next waypoint, NavPoints[1], and set the throttle to 1.0 (for now). Our logic so far looks like:
I thought you said this was hard?
Here's what it looks like in-game:
Next time, we'll make the throttle smarter and make use of the handbrake.
The UI for our placeable object commands isn’t quite ready yet (it seems everyone’s suddenly a critic), but this post we’ll introduce similar concepts while implementing another desperately needed feature: transitioning between maps. I’m getting a little tired of having to close down the UDK and reconfiguring the Unreal FrontEnd just to change maps:
We’ll provide the option to choose a map with a flyout menu built in Flash. But to achieve our objective of keeping the maps independent of the BattleMap mod, we can’t hard code the map list into the menu. Therefore, we’ll store the list in a config file and pass it to the HUD on startup.
Flash
Once again, our friends at MonsterLayer.com produced some gorgeous assets for us in an astonishingly short amount of time. I’ll do my best to break down how they built our menu, but you can certainly create any style of menu that suits you. All that’s important is that the asset names match the code.
Below is a snapshot of an individual menu button. This will be duplicated multiple times: one for each map.
The “Scripts” layer simply contains:
stop();
The second frame of the background later changes the color. That will become the “hover” when moused over. (Yes, Flash does include a Button type. However, we had multiple issues trying to get it to work correctly so we went with the programmer’s approach of just coding it ourselves.)
Make sure the Text object is named “ButtonText” and save the symbol as a MovieClip named “MapButton”.
Create another button the exact same way:
Name this MovieClip “CloseButton”.
Next, create a menu that contains your button objects:
Name the map button instances “MapButton0”, “MapButton1”, etc. Name the close button instance “CloseButton”. Save this MovieClip as “MapList”.
Create another MovieClip called “MapListMenu”. Add an instance of MapList named “MapListInst”. Create a motion tween of it moving onto the stage:
The scripts on the 1st and last frames both simply contain:
stop();
Now, add an instance of MapListMenu to your root movie named “MapListMenuInst”. Position it so that it starts outside the scene, but ends inside at the end of its animation. (Edit In Place comes in real handy here.)
We need one more object. Transitioning between maps can take a few seconds to initiate. We should provide a visual clue that the transition has begun and the UDK isn’t hung. Create an object named “Overlay”. I drew a giant semi-transparent background with the ever-polite instructions “Please Wait…”:
Add an instance of your Overlay object to your root movie as “OverlayInst”.
ActionScript
Now, we need to wire it all up. Add the following to your root ActionScript. It initializes an array to store the map list, hides the Overlay and sets up mouse events for the buttons. The click event for the map buttons will show the Overlay on top, toggle off the menu and pass the chosen map back to UnrealScript:
Next, add the following InitMapList() function. This will be called by UnrealScript to pass in the map list and setup the map buttons:
function InitMapList(Param1:Array)
{
Maps = Param1;
//Show only buttons with maps
for (i=0; i<Maps.length; i++)
{
MapListMenuInst.MapListInst["MapButton"+i].ButtonText.text = Maps[i]["Name"];
MapListMenuInst.MapListInst["MapButton"+i]._visible = true;
}
}
Finally, add the ShowMapList() function to toggle the menu on/off:
function ShowMapList()
{
if (MapListMenuInst._currentframe == 1)
{
MapListMenuInst.swapDepths(_root.getNextHighestDepth());
CursorInst.swapDepths(_root.getNextHighestDepth());
MapListMenuInst.gotoAndPlay(2);
}
else
MapListMenuInst.gotoAndStop(1);
}
Republish the .swf file then open up the UDK, find the BattleMapHud package, and reimport BMHud.
BattleMapConfig.uc
Now, let’s setup a config file to store the map names. The UDK builds in configuration file functionality into the base Object class. That means that every class can implement its own config file. Mougli’s portfolio includes a very good tutorial on UDK configuration files. To sum up, any variable declared globally in a class becomes an entry in its config file.
Here’s the code for a simple object containing an array of MapItem structs:
class BattleMapConfig extends Object config(BattleMap); struct MapItem
{
var config string Name;
var config string File;
}; var config array <MapItem> Maps;
The “config(BattleMap)” directive tells the UDK that this object will read and write to a BattleMap.ini file.
DefaultBattleMap.ini
In your /UDKGame/Config directory, create a new .ini file called “DefaultBattleMap.ini” and enter the titles and file names of your maps. For example, mine looks like:
The format should look very familiar, it’s similar to DefaultInput.ini where we include new key bindings. Notice that the section heading is the name of our class.
DefaultInput.ini
Speaking of DefaultInput.ini, while we’re here go ahead and add a key binding to toggle our menu:
(In case you’re curious, the period in front of .Bindings means that duplicate entries are allowed.)
BattleMapPlayerController.uc
Here, we simply need to instantiate our new BattleMapConfig class, which will cause it to automatically initialize its variables from the config file:
var BattleMapConfig BMConfig; simulated function PostBeginPlay()
{
super.PostBeginPlay();
BMConfig = new class'BattleMapConfig';
}
BattleMapHUD.uc
Inside PostBeginPlay(), add a call to a CallInitMapList() function right after the CrosshairMovie.Initialize() statement and pass in the newly loaded Map array:
Create a new command to tell the HUD to toggle the menu and another to tell the UDK to load a new map:
exec function BMShowMapList()
{
if (WorldInfo.NetMode == NM_Standalone || WorldInfo.NetMode == NM_ListenServer)
{
BattleMapHUD(myHUD).CrossHairMovie.CallShowMapList();
}
} exec function BMOpenMap(string MapFile)
{
WorldInfo.Game.ProcessServerTravel(MapFile);
}
BattleMapGfxHud.uc
First, create two new wrapper functions for calling the HUD’s InitMapList() and ShowMapList():
function CallInitMapList( array <MapItem> Param1 )
{
ActionScriptVoid("InitMapList");
} function CallShowMapList()
{
ActionScriptVoid("ShowMapList");
}
Finally, create two receiver functions called by the HUD to execute our BMOpenMap() command and the Quit command:
function OpenMap(string MapFile)
{
ConsoleCommand("BMOpenMap " @ MapFile);
} function CloseMap()
{
ConsoleCommand("Quit");
}
That’s it! If everything went smoothly, hitting Esc will now pop up a menu of map choices. Clicking on a map name will transition to a new map. Clicking the close button will cleanly exit the UDK.
There won't be a blog post addressing a new feature this coming weekend. But I didn't want to leave you without something to play with, so I'm addressing a few questions asked in the comments:
Our friends at MonsterLayer.com gave us permission to give away the assets they created for us. Copy BattleMapItems.upk to your /UDKGame/Content/Misc directory. It includes:
Bed
Bedroll
Chair
Chest
Drawer
Ladder
Small Table
Table
Here's a couple of clips of the BattleMap in use from our last session:
And finally, according to the UDK Licensing FAQ, as long as you're not making any money off your creation you are free to package and distribute your project.
No new groundbreaking features this post. We’re going to spend this time tinkering with what we have a bit.
First order of business is fixing BMInteractObject(), which selects (picks up and drags) placeable objects. It appears to work well, except when clicking over both a map object and a HUD object. In that case, it never lets go of the HUD object. This is caused by a small bug.
Actually, the only reason it works at all is due to that bug. The code is divided into two sections: a drop and a pickup. In the drop code at the top, the switch statement checks for “CrossHairMovie” to see if the HUD has an object selected. That is incorrect, as the code checks the name of the OBJECT not the name of the CLASS. That causes the drop section to always fail for the HUD, which is actually a good thing. The code then falls to the pickup section, which calls the HUD’s InteractObject() again if a map object was not selected.
If we fix the reference in the drop section, another problem appears: the cursors never lets go of any HUD object. The HUD’s InteractObject() is self-contained with its own drop and pickup code, including a special clause to ensure it doesn’t pick up the same thing it dropped. If we try to drop and pick up in two separate calls, it always picks up what it dropped.
The easy answer was to move the call to the HUD’s InteractObject() to the top and always do it first. That’s less than ideal however, since HUD objects overlay map objects. We had to move a HUD object out of the way in order to click on a map object. So, the HUD’s InteractObject() call needs to be at the end. But what if we clicked on a map object? We passed a parameter to InteractObject() to only drop what’s selected and not select anything new.
Then it worked! And created a new problem. If we moved a map object under a HUD object, or a HUD object directly over a map object, and clicked to let go it would hot-swap between the two. We couldn’t let go unless there was nothing else to select.
The final solution is to simply do one or the other. Swapping was an interesting bit of code, but simply not feasible during gameplay. Here’s the final BMInteractObject():
BattleMapPlayerInput.uc
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);
break;
case "GfxHud":
BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(false);
break;
}
ObjectSelected = none;
}
else
{
//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;
}
//if no map object found, check for a HUD object
if (ObjectSelected == none)
if(BattleMapHUD(myHUD).CrossHairMovie.CallInteractObject(true) > 0)
ObjectSelected = BattleMapHUD(myHUD).CrossHairMovie;
}
}
}
BattleMapGfxHud.uc
Add a parameter to CallInteractObject:
function int CallInteractObject(bool Param1)
ActionScript
In InteractObject(), update the definition to include the new parameter:
function InteractObject(Param1:Boolean)
And preface the loop that checks for objects under the mouse with the passed parameter:
//find new object to interact with
if (Param1)
for (var MousedObject in _root)
Finally, update the call in DeleteObject() to include the parameter:
InteractObject(true);
Now, let’s take what we’ve done with Zones and create map tokens for our monsters and players. Even though we place miniatures on the table during gameplay, it is still helpful to represent creatures within the battlemap for several reasons:
The DM doesn’t have to move around the table to reposition minis. He can move a token with a click then ask a player (or demand, depending on the DM) to move the miniature.
The DM can easily see the position of creatures without moving around for a better view. I can’t tell you how many times I’ve declared an invalid opportunity attack by an NPC, sparking a bewildered debate among the players. Or missed an opportunity attack on players, all due to my skewed view from the end of the table.
The minis are lit by whatever texture they happen to be standing on. A white token would “spotlight” the minis, despite standing on black rock, yellow stone or green goo.
Flash
Create a new token symbol within Flash. I simply made a 256x256 white circle with a black border. Don’t forget to check “Export for ActionScript”:
Next, add a block of Dynamic Text with a “P1” placeholder. Give it a variable name of “TokenText”:
You will also need to Embed the font (button next to Style). I chose only Uppercase and Numerals.
ActionScript
Add new global variables to the top:
var DefaultZoneScale:Number = 70;
var DefaultTokenScale:Number = 50;
var Tokens:Number = 0;
Add a “Token” case to the Spawn() function:
case "Token":
SpawnToken();
break;
Add the new SpawnToken() function below. It’s a separate function like SpawnZone() just to be consistent, though it didn’t need to be. SpawnZone() had additional parameters because the SwapObject() function replaced a zone by spawning a new zone with the same properties as the old one. When we swap a token, we’ll simply be changing the text.
For now, we just assign a counter to the token text. When we implement a pop-up UI within the HUD for manipulating objects, we'll pass in typed text. For now, just note the P# or M# next to the creature in your combat tracker.
We’re also ensuring that the cursor is always on top. Since a token will never need to remember what layer it was on (like swapping a zone) we just grab the highest available depth. That, of course, places it over the cursor so we need to bump that as well.
Now, update the InteractObject() function so it will grab tokens as well. We want it to work exactly the same as selecting a Zone, so we simply need to update the case statement:
case "Zone":
case "Tokn":
//find zones/tokens, but not the one we had clicked on
The same is true for the ModifyObject() function. Modifying a token will manipulate its scale just like a zone. However, we want to implement a new feature while we’re at it. If we size a token to a 1x1 square on our battlemap, it is highly likely that additional tokens will need to be that size as well. We already used the DefaultTokenScale global variable when spawning a new token, so we need to set that value here when modifying an object:
The SwapObject() function will simply update the text on the token, alternating between “P#” (PC) and “M#” (monster):
case "Tokn":
switch(_root[ObjectSelected].TokenText.substr(0, 1))
{
case "P":
_root[ObjectSelected].TokenText = "M" + _root[ObjectSelected].TokenText.substr(1);
break;
case "M":
_root[ObjectSelected].TokenText = "P" + _root[ObjectSelected].TokenText.substr(1);
break;
}
break;
Finally, let’s implement the same default scaling for zones. We've already done most of the work along the way. In SpawnZone(), change the ZoneScale variable to:
var ZoneScale = (Param5 != undefined) ? Param5 : DefaultZoneScale;
BattleMapPlayerInput.uc
Add a new case to BMSpawn():
case "Token":
BattleMapHUD(myHUD).CrossHairMovie.CallSpawn("Token");
DefaultInput.ini
Add a new key binding:
.Bindings=(Name="N",Command="BMSpawn Token")
Let’s enhance our blood decal a bit. We’ll borrow the same movement functionality as our torch and apply it to the blood decal.
BattleMapBlood.uc
Insert the following block into the BattleMapBlood class between the declaration and PostBeginPlay(). This code is nearly identical to the Torch code (we changed the names and removed the VerticalOffset that the torch light required):
var repnotify Vector BloodLoc,BloodStopLoc; replication
{
if (bNetDirty)
BloodLoc,BloodStopLoc;
} // called on clients when BloodStopLoc gets replicated
simulated event ReplicatedEvent( name VarName )
{
if (VarName == nameof(BloodLoc) )
SetLocation( BloodLoc );
else if (VarName == nameof(BloodStopLoc) )
StopBlood( BloodStopLoc );
else
super.ReplicatedEvent( VarName );
} // this function should be run on both clients and server
simulated function MoveBlood( )
{
bCollideWorld = false;
SetCollision(false, false);
GotoState( 'BloodMoving' );
} simulated function StopBlood( Vector NewBloodStopLoc )
{
bCollideWorld = true;
SetCollision(true, true);
GotoState('BloodStopped'); BloodStopLoc = NewBloodStopLoc;
SetLocation(NewBloodStopLoc);
} simulated state BloodMoving
{
simulated event Tick( float DeltaTime )
{
// move the blood...
BloodLoc = BattleMapPlayerController(Owner).MouseOrigin;
SetLocation(BloodLoc);
}
} simulated state BloodStopped
{
simulated event Tick( float DeltaTime )
{
// stop the blood...
}
}
BattleMapPlayerInput.uc
In BMInteractObject(), add a new variable:
local BattleMapBlood Bl;
Add a blood case to the “drop” switch statement:
case "Blood":
Bl = BattleMapBlood(ObjectSelected);
Bl.StopBlood(MouseOrigin);
break;
And a blood case to the “select” switch statement:
case "Blood":
foreach DynamicActors(class'BattleMapBlood', Bl)
if (Bl.Name == ObjectUnderMouse)
{
ObjectSelected = Bl;
Bl.MoveBlood();
}
break;
Next, we’ll borrow the same sizing functionality as our zones and tokens. Add the following new variable to the top:
var float DefaultBloodSize;
In BMModifyObject(), add a new local variable:
local BattleMapBlood B;
And a blood case to the switch statement. We’ll manipulate the decal’s size:
Next, update the “Blood” case in the switch statement to resize after spawning. The call to SetLocation() is simply a dirty way to force a replication: