Floating ActorTips

From Oldunreal-Wiki
Jump to navigation Jump to search
This tutorial explains the basics of adding floating pop-up labels in the hud that track objects in the game, much like the info frames in DeusEx (if you enhance the rendering quite a bit). I call them ActorTips.

First of all we need to create a new HUD-type that implements the ActorTip feature. Here we'll add it to the normal UT ChallengeHUD, but you could base it on any HUD-type available to you:

 class TipHUD expands ChallengeHUD;

Now, we need to decide which object(s) we're going to label. You can of course make that selection according to any criteria you wish (your friends, objects in a certain state, or ones you've previously somehow selected), but for this example we'll just make a guess at which object the player is most likely "looking" at:

simulated function Actor GetFocusedActor()
{
 local vector Look, EyePos;
 local Actor Candidate, Winner;
 local float Score, HiScore;
 // Find out where the camera is, and where it's looking:
 EyePos = Owner.Location;
 EyePos.Z += Pawn(Owner).EyeHeight;
 Look = vector(Pawn(Owner).ViewRotation);
 // Iterate through all actors closer than 200 UUs to the player,
 // trying to find the one most directly in front of him:
 HiScore = 0;
 foreach VisibleActors(class'Actor', Candidate, 200.0, EyePos)
 {
   // The dot product of two normalised vectors is a nice
   // measure of the angle between them:
   // (1 = perfect match, 0 = right angles, -1 = direct opposites)
   Score = Normal(Candidate.Location - EyePos) dot Look;
   // Ignore actors too far to the sides (too low scores):
   if (Score >= 0.9) // adjust this threshold to taste
   {
     // See if this candidate is an improvement:
     if (Score > HiScore)
     {
       HiScore = Score;
       Winner = Candidate;
     }
   }
 }
 return Winner;
}

We also need to work out where on the screen an object will appear, based on its 3D coordiantes, so we can position our floating graphic correctly on top of it. (The math is explained in the appendix at the end.)

simulated function vector MapToScreen(vector TargetPos, Canvas Canvas)
{
 local vector Result, EyePos;
 local rotator Look, Focus;
 local float TanFOV, dYaw, dPitch, scale;
 // Again, our current point of view is sort of essential:
 EyePos = Owner.Location;
 EyePos.Z += Pawn(Owner).EyeHeight;
 // Find out where we're facing, but his time as a 'rotator' providing
 // angles rather than a vector:
 // (The conversion to vector and back is just a suboptimal but robust
 // way to make sure we get nice signed values for the pitch, instead
 // of wrap-around from 0 to 65535 at the horizon).
 Look = rotator(vector(Pawn(owner).ViewRotation));
 // Find the corresponding rotator angles of the direction from
 // the camera to our target:
 Focus = rotator(TargetPos - EyePos);
 // Now find out how much the target angles are offset from the
 // look angles. (And while we're at it, convert to radians, since
 // that's what Unreal's trig functions work with):
 dYaw = (focus.yaw-look.yaw) * Pi / 32768.0;
 dPitch = (focus.pitch-look.pitch) * Pi / 32768.0;
 // The field of view obviously affects how things are projected
 // to the screen, as does the canvas resolution.
 // In fact, the two can be combined into a single scaling factor
 // that tells us all we need to know.
 Scale = (Canvas.ClipX/2) / tan(Pawn(Owner).FOVangle * Pi / 360.0);
 // Finally calculate actual screen coordinates:
 Result.x = Canvas.ClipX/2 + tan(dYaw) * Scale;
 Result.y = Canvas.ClipY/2 - tan(dPitch) * Scale;
 return Result;
}

Now that we have the tools we need, we do the actual drawing. For this tutorial we just print the name of the object over it, but you could write any other information, or draw icons or frames or anything you like. This function also wraps up our entire HUD module in a single call:

simulated function DrawActorTip(Canvas Canvas)
{
 local Actor FocusedActor;
 local vector ScreenPos;
 // Find Target
 FocusedActor = GetFocusedActor();
 // Label Target
 if (FocusedActor != None)
 {
   Canvas.DrawColor = WhiteColor;
   Canvas.Style = ERenderStyle.STY_Translucent;
   Canvas.Font = MyFonts.GetSmallFont( Canvas.ClipX );
   ScreenPos = MapToScreen(FocusedActor.Location, Canvas);
   Canvas.SetPos(Screenpos.x, Screenpos.y);
   Canvas.DrawText(FocusedActor.Name);
 }
}

Finally we need to make sure our drawing routine is actually called each time the HUD is rendered:

// Override the old HUD-redrawing function:
simulated function PostRender(Canvas Canvas)
{
 // -But make sure all the old drawing is still done first:
 // (If you want to, that is)
 super.PostRender(Canvas);
 // Then add our ActorTip on top:
 DrawActorTip(Canvas);
}

And it's done!

APPENDIX: The math of projecting a direction-offset onto the screen.

The problem is to find out where the vector from the camera to the target will intersect the screen. We can solve this for X and Y separately. Considering X first, looking at the figure below, our problem is to figure out dx:

From the definition of the tangent we know that

                 dx
tan(dYaw) = ---
                  z

and that

                  ClipX/2
tan(FOV/2) = ----------
                       z

These easily reduce to:

                        ClipX/2
dx = tan(dYaw) -----------------
                      tan(FOV/2)

This is almost our code exactly. All we add is some conversion to radians and offsetting the origin to the centre of the canvas.

We then do the same thing for Y versus Pitch. Note that we then use same scaling fraction (with ClipX, not ClipY), since we don't have any separate vertical FOV. Errata Since publishing the tute the author has informed us the following: "I've discovered a mistake in it: my way of projecting the position onto the screen is incorrect: It works OK around horizontal, but when looking steeply up or down it becomes clear that the assumption that you can solve for X and Y separately doesn't hold" We'll try to resolve this soon.