UDN
Search public documentation:
DevelopmentKitGemsAddingOnScreenIndicators
日本語訳
中国翻译
한국어
Interested in the Unreal Engine?
Visit the Unreal Technology site.
Looking for jobs and company info?
Check out the Epic games site.
Questions about support via UDN?
Contact the UDN Staff
中国翻译
한국어
Interested in the Unreal Engine?
Visit the Unreal Technology site.
Looking for jobs and company info?
Check out the Epic games site.
Questions about support via UDN?
Contact the UDN Staff
UE3 Home > Unreal Development Kit Gems > Adding on screen indicators
Adding on screen indicators
Last tested against UDK May, 2011
PC compatible
Overview
By default, Unreal Tournament 3 has a simplistic on screen indicator to show the player's name when in front of the camera. This development kit gem will show you how to create on screen indicators which point to targets on screen. If the target is off screen because the target is behind the camera then the on screen indicator will stay at the bottom of the screen. If the target is off screen because the target is out of the camera's frustum, then the on screen indicator will either stay on the edges of the screen and point where the target is. The on screen indicator also fades out if the target is currently not being rendered on screen. Lastly, the on screen indicators also change color depending on the team that the target is on.
- These pawns are visible on screen, and are on the blue team. Their on screen indicators shift slightly to the left or the right and the arrow points accordingly.
- This pawn is hidden behind the bridge, thus the on screen indicator goes transparent.
- This a team mate, who is out of the camera frustum's view and is out of sight.
- These pawns are behind the player and out of sight.
Indicator material
The on screen indicator material is responsible for a few things. It's responsible for the rotation, opacity and team colorization. Here is the texture used for the on screen indicator. The below image shows the texture's individual channels.
- This is the gray scale diffuse used for the on screen indicator. It has a lot of clear space otherwise when the texture rotates artifacts may appear due to texture wrapping.
- This is the mask used to set the opacity of the icon in the middle of the on screen indicator.
- This is the mask used to create a shadow for the on screen indicator.
- This is the mask used as the opacity for the on screen indicator.
Unrealscript
SRadarInfo struct
A struct was used to contain information about each of the on screen indicators.- UTPawn - A reference to the pawn that this on screen indicator points to.
- MaterialInstanceConstant - An instance of the material used by the on screen indicator.
- DeleteMe - A boolean for deletion purposes.
- Offset - Offset of the on screen indicator used for animation purposes.
- Opacity - Current opacity of the on screen indicator.
struct SRadarInfo { var UTPawn UTPawn; var MaterialInstanceConstant MaterialInstanceConstant; var bool DeleteMe; var Vector2D Offset; var float Opacity; };
AddPostRenderedActor function
By default, Unreal Tournament 3 uses post rendered actors to render the existing name indicators. This function removes that behavior just for the UTPawn.function AddPostRenderedActor(Actor A) { // Remove post render call for UTPawns as we don't want the name bubbles showing if (UTPawn(A) != None) { return; } Super.AddPostRenderedActor(A); }
PostRender event
The post render event in Unrealscript is called by the engine to allow Unrealscript an opportunity to render textures, materials or text onto the screen. This is where the bulk of the logic is for this development kit gem; thus will be split up into smaller sections.Update pass
In this pass, the radar info's are all tested for deletion by first setting DeleteMe to true. Next all UT Pawns are iterated over using ForEach. For all pawns that isn't owned by the player, first check if a radar info references it. This is done by attempting to find a valid index in the RadarInfo array. If the index is valid and the pawn has health then we set DeleteMe to false as it passes the deletion check. If the index is invalid and the pawn has health, then a new radar info is created. When a new radar info is created, a new material instance is also created for that radar info. The team color is also set, and DeleteMe is set false as it also passes the deletion check.// Set all radar infos to delete if not found for (i = 0; i < RadarInfo.Length; ++i) { RadarInfo[i].DeleteMe = true; } // Update the radar infos and see if we need to add or remove any ForEach DynamicActors(class'UTPawn', UTPawn) { if (UTPawn != PlayerOwner.Pawn) { Index = RadarInfo.Find('UTPawn', UTPawn); // This pawn was not found in our radar infos, so add it if (Index == INDEX_NONE && UTPawn.Health > 0) { i = RadarInfo.Length; RadarInfo.Length = RadarInfo.Length + 1; RadarInfo[i].UTPawn = UTPawn; RadarInfo[i].MaterialInstanceConstant = new () class'MaterialInstanceConstant'; if (RadarInfo[i].MaterialInstanceConstant != None) { RadarInfo[i].MaterialInstanceConstant.SetParent(Material'GemOnscreenRadarContent.PointerMaterial'); if (UTPawn.PlayerReplicationInfo != None && UTPawn.PlayerReplicationInfo.Team != None) { TeamLinearColor = (UTPawn.PlayerReplicationInfo.Team.TeamIndex == 0) ? Default.RedLinearColor : Default.BlueLinearColor; RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', TeamLinearColor); } else { RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', Default.DMLinearColor); } } RadarInfo[i].DeleteMe = false; } else if (UTPawn.Health > 0) { RadarInfo[Index].DeleteMe = false; } } }
Rendering pass
Before the pass is calculated, the on screen indicator size is first calculated and stored as PointerSize. In this implementation, this is relative to the screen resolution width. The camera view direction is also derived from the camera location and rotation. The camera view direction is used to detect if a pawn is behind the player or not. In this pass radar info's are rendered if DeleteMe is false. Otherwise they are removed from the radar info array. When the on screen indicator is rendered, it is first checked if the associated pawn has been rendered in the last 0.1 second. If it hasn't, then it's opacity is linearly interpolated down to 40%. Otherwise it is linearly interpolated up to 100%. When the opacity is calculated, it is then set in the material instance constant. The direction between the player's pawn and the pawn is calculated. The dot product between the camera direction and the player pawn to pawn direction determines if the player is currently looking at the pawn or not. In the image below, the red arrow represents the camera direction. The green arrow represents the direction of the player pawn to the pawn. The dot product will be a value between 1.f and 0.f, thus will be in front of the camera. The blue arrow represents the direction of the player pawn to another pawn. The dot product will be a value between 0.f and -1.f, thus will be behind the camera.// Handle rendering of all of the radar infos PointerSize = Canvas.ClipX * 0.083f; PlayerOwner.GetPlayerViewPoint(CameraLocation, CameraRotation); CameraViewDirection = Vector(CameraRotation); for (i = 0; i < RadarInfo.Length; ++i) { if (!RadarInfo[i].DeleteMe) { if (RadarInfo[i].UTPawn != None && RadarInfo[i].MaterialInstanceConstant != None) { // Handle the opacity of the pointer. If the player cannot see this pawn, // then fade it out half way, otherwise if he can, fade it in if (WorldInfo.TimeSeconds - RadarInfo[i].UTPawn.LastRenderTime > 0.1f) { // Player has not seen this pawn in the last 0.1 seconds RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 0.4f, RenderDelta * 4.f); } else { // Player has seen this pawn in the last 0.1 seconds RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 1.f, RenderDelta * 4.f); } // Apply the opacity RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Opacity', RadarInfo[i].Opacity); // Get the direction from the player's pawn to the pawn PawnDirection = Normal(RadarInfo[i].UTPawn.Location - PlayerOwner.Pawn.Location); // Check if the pawn is in front of me if (PawnDirection dot CameraViewDirection >= 0.f) { // Handle rendering the on screen indicator when the actor is in front of the camera } else { // Handle rendering the on screen indicator when the actor is behind the camera } } } else { // Null the variables previous stored so garbage collection can occur RadarInfo[i].UTPawn = None; RadarInfo[i].MaterialInstanceConstant = None; // Remove from the radar info array RadarInfo.Remove(i, 1); // Back step one, to maintain the for loop --i; } }
Rendering in front of the camera pass
First the WorldHUDLocation is calculated by adding together the pawn's location with the collision height as an offset. This is then projected into screen coordinates. If the screen coordinate is on the left hand side of the screen an offset is interpolated to the left; or if it is on the right hand side of the screen an offset is interpolated to the right. This provides a little bit of animation to the on screen indicator. The offset is clamped to ensure that it doesn't get too large in either direction. The rotation of the material instance is then calculated and set. Finally, the on screen indicator is rendered onto the screen.// Get the world HUD location, which is just above the pawn's head WorldHUDLocation = RadarInfo[i].UTPawn.Location + (RadarInfo[i].UTPawn.GetCollisionHeight() * Vect(0.f, 0.f, 1.f)); // Project the world HUD location into screen HUD location ScreenHUDLocation = Canvas.Project(WorldHUDLocation); // If the screen HUD location is more to the right, then swing it to the left if (ScreenHUDLocation.X > (Canvas.ClipX * 0.5f)) { RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f; } else { // If the screen HUD location is more to the left, then swing it to the right RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f; } RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f); // Set the rotation of the material icon ActualPointerLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8) + RadarInfo[i].Offset.X; ActualPointerLocation.Y = Clamp(ScreenHUDLocation.Y - PointerSize + RadarInfo[i].Offset.Y, 8, Canvas.ClipY - 8 - PointerSize) + (PointerSize * 0.5f); RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation)); // Draw the material pointer Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f)); Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f);
Rendering behind the camera pass
For actors behind the camera, the on screen indicator is always at the bottom of the screen. While it is possible to still use the project function, it will return results that needs to modified. This is why the projected horizontal coordinates is inversed. If the horizontal screen coordinates is on the left edge then the horizontal offset is pushed to the right and vice versa. This ensures that the on screen indicator is always on screen. Similar rotation calculations are done and applied to the material instance. Finally, the on screen indicator is rendered onto the screen.// Project the pawn's location ScreenHUDLocation = Canvas.Project(RadarInfo[i].UTPawn.Location); // Inverse the Screen HUD location ScreenHUDLocation.X = Canvas.ClipX - ScreenHUDLocation.X; // If the screen HUD location is on the right edge, then swing it to the left if (ScreenHUDLocation.X > (Canvas.ClipX - 8)) { RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f; RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f); } else if (ScreenHUDLocation.X < 8) { // If the screen HUD location is on the left edge, then swing it to the right RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f; RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f); } else { // If the screen HUD location is somewhere in the middle, then straighten it up RadarInfo[i].Offset.X = Lerp(RadarInfo[i].Offset.X, 0.f, 4.f * RenderDelta); } // Set the screen HUD location ScreenHUDLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8); ScreenHUDLocation.Y = Canvas.ClipY - 8; // Set the actual pointer location ActualPointerLocation.X = ScreenHUDLocation.X + RadarInfo[i].Offset.X; ActualPointerLocation.Y = ScreenHUDLocation.Y - (PointerSize * 0.5f); // Set the rotation of the material icon RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation)); // Draw the material pointer Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f)); Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f);
Angle calculation
In this angle calculation method, the black arrow is the reference vector. The two vectors form either the red line or the green line. The angle calculated is shown as the blue arc. The calculations are done in radians since the material expects the rotation value in radians. Some early checks are done to return simple results such as 0.f (0 degrees), Pi (180 degrees), Pi * 1.5f (270 degrees) and Pi * 0.5f (90 degrees). This method is a faster than the quadrant checking method (using SOH CAH TOA) and the other acos method.function float GetAngle(Vector PointB, Vector PointC) { // Check if angle can easily be determined if it is up or down if (PointB.X == PointC.X) { return (PointB.Y < PointC.Y) ? Pi : 0.f; } // Check if angle can easily be determined if it is left or right if (PointB.Y == PointC.Y) { return (PointB.X < PointC.X) ? (Pi * 1.5f) : (Pi * 0.5f); } return (2.f * Pi) - atan2(PointB.X - PointC.X, PointB.Y - PointC.Y); }
Completed Unrealscript class
This is the completed Unrealscript class; shown here for clarity on how to piece it all together.class UTRadarHUD extends UTTeamHUD; struct SRadarInfo { var UTPawn UTPawn; var MaterialInstanceConstant MaterialInstanceConstant; var bool DeleteMe; var Vector2D Offset; var float Opacity; }; var array<SRadarInfo> RadarInfo; function AddPostRenderedActor(Actor A) { // Remove post render call for UTPawns as we don't want the name bubbles showing if (UTPawn(A) != None) { return; } Super.AddPostRenderedActor(A); } event PostRender() { local int i, Index; local Vector WorldHUDLocation, ScreenHUDLocation, ActualPointerLocation, CameraViewDirection, PawnDirection, CameraLocation; local Rotator CameraRotation; local UTPawn UTPawn; local LinearColor TeamLinearColor; local float PointerSize; if (PlayerOwner == None || PlayerOwner.Pawn == None) { return; } // Set up the render delta RenderDelta = WorldInfo.TimeSeconds - LastHUDRenderTime; // Set all radar infos to delete if not found for (i = 0; i < RadarInfo.Length; ++i) { RadarInfo[i].DeleteMe = true; } // Update the radar infos and see if we need to add or remove any ForEach DynamicActors(class'UTPawn', UTPawn) { if (UTPawn != PlayerOwner.Pawn) { Index = RadarInfo.Find('UTPawn', UTPawn); // This pawn was not found in our radar infos, so add it if (Index == INDEX_NONE && UTPawn.Health > 0) { i = RadarInfo.Length; RadarInfo.Length = RadarInfo.Length + 1; RadarInfo[i].UTPawn = UTPawn; RadarInfo[i].MaterialInstanceConstant = new () class'MaterialInstanceConstant'; if (RadarInfo[i].MaterialInstanceConstant != None) { RadarInfo[i].MaterialInstanceConstant.SetParent(Material'GemOnscreenRadarContent.PointerMaterial'); if (UTPawn.PlayerReplicationInfo != None && UTPawn.PlayerReplicationInfo.Team != None) { TeamLinearColor = (UTPawn.PlayerReplicationInfo.Team.TeamIndex == 0) ? Default.RedLinearColor : Default.BlueLinearColor; RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', TeamLinearColor); } else { RadarInfo[i].MaterialInstanceConstant.SetVectorParameterValue('TeamColor', Default.DMLinearColor); } } RadarInfo[i].DeleteMe = false; } else if (UTPawn.Health > 0) { RadarInfo[Index].DeleteMe = false; } } } // Handle rendering of all of the radar infos PointerSize = Canvas.ClipX * 0.083f; PlayerOwner.GetPlayerViewPoint(CameraLocation, CameraRotation); CameraViewDirection = Vector(CameraRotation); for (i = 0; i < RadarInfo.Length; ++i) { if (!RadarInfo[i].DeleteMe) { if (RadarInfo[i].UTPawn != None && RadarInfo[i].MaterialInstanceConstant != None) { // Handle the opacity of the pointer. If the player cannot see this pawn, // then fade it out half way, otherwise if he can, fade it in if (WorldInfo.TimeSeconds - RadarInfo[i].UTPawn.LastRenderTime > 0.1f) { // Player has not seen this pawn in the last 0.1 seconds RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 0.4f, RenderDelta * 4.f); } else { // Player has seen this pawn in the last 0.1 seconds RadarInfo[i].Opacity = Lerp(RadarInfo[i].Opacity, 1.f, RenderDelta * 4.f); } // Apply the opacity RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Opacity', RadarInfo[i].Opacity); // Get the direction from the player's pawn to the pawn PawnDirection = Normal(RadarInfo[i].UTPawn.Location - PlayerOwner.Pawn.Location); // Check if the pawn is in front of me if (PawnDirection dot CameraViewDirection >= 0.f) { // Get the world HUD location, which is just above the pawn's head WorldHUDLocation = RadarInfo[i].UTPawn.Location + (RadarInfo[i].UTPawn.GetCollisionHeight() * Vect(0.f, 0.f, 1.f)); // Project the world HUD location into screen HUD location ScreenHUDLocation = Canvas.Project(WorldHUDLocation); // If the screen HUD location is more to the right, then swing it to the left if (ScreenHUDLocation.X > (Canvas.ClipX * 0.5f)) { RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f; } else { // If the screen HUD location is more to the left, then swing it to the right RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f; } RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f); // Set the rotation of the material icon ActualPointerLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8) + RadarInfo[i].Offset.X; ActualPointerLocation.Y = Clamp(ScreenHUDLocation.Y - PointerSize + RadarInfo[i].Offset.Y, 8, Canvas.ClipY - 8 - PointerSize) + (PointerSize * 0.5f); RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation)); // Draw the material pointer Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f)); Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f); } else { // Handle rendering the on screen indicator when the actor is behind the camera // Project the pawn's location ScreenHUDLocation = Canvas.Project(RadarInfo[i].UTPawn.Location); // Inverse the Screen HUD location ScreenHUDLocation.X = Canvas.ClipX - ScreenHUDLocation.X; // If the screen HUD location is on the right edge, then swing it to the left if (ScreenHUDLocation.X > (Canvas.ClipX - 8)) { RadarInfo[i].Offset.X -= PointerSize * RenderDelta * 4.f; RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f); } else if (ScreenHUDLocation.X < 8) { // If the screen HUD location is on the left edge, then swing it to the right RadarInfo[i].Offset.X += PointerSize * RenderDelta * 4.f; RadarInfo[i].Offset.X = FClamp(RadarInfo[i].Offset.X, PointerSize * -0.5f, PointerSize * 0.5f); } else { // If the screen HUD location is somewhere in the middle, then straighten it up RadarInfo[i].Offset.X = Lerp(RadarInfo[i].Offset.X, 0.f, 4.f * RenderDelta); } // Set the screen HUD location ScreenHUDLocation.X = Clamp(ScreenHUDLocation.X, 8, Canvas.ClipX - 8); ScreenHUDLocation.Y = Canvas.ClipY - 8; // Set the actual pointer location ActualPointerLocation.X = ScreenHUDLocation.X + RadarInfo[i].Offset.X; ActualPointerLocation.Y = ScreenHUDLocation.Y - (PointerSize * 0.5f); // Set the rotation of the material icon RadarInfo[i].MaterialInstanceConstant.SetScalarParameterValue('Rotation', GetAngle(ActualPointerLocation, ScreenHUDLocation)); // Draw the material pointer Canvas.SetPos(ActualPointerLocation.X - (PointerSize * 0.5f), ActualPointerLocation.Y - (PointerSize * 0.5f)); Canvas.DrawMaterialTile(RadarInfo[i].MaterialInstanceConstant, PointerSize, PointerSize, 0.f, 0.f, 1.f, 1.f); } } } else { // Null the variables previous stored so garbage collection can occur RadarInfo[i].UTPawn = None; RadarInfo[i].MaterialInstanceConstant = None; // Remove from the radar info array RadarInfo.Remove(i, 1); // Back step one, to maintain the for loop --i; } } // Setup the render delta LastHUDRenderTime = WorldInfo.TimeSeconds; Super.PostRender(); } function float GetAngle(Vector PointB, Vector PointC) { // Check if angle can easily be determined if it is up or down if (PointB.X == PointC.X) { return (PointB.Y < PointC.Y) ? Pi : 0.f; } // Check if angle can easily be determined if it is left or right if (PointB.Y == PointC.Y) { return (PointB.X < PointC.X) ? (Pi * 1.5f) : (Pi * 0.5f); } return (2.f * Pi) - atan2(PointB.X - PointC.X, PointB.Y - PointC.Y); } defaultproperties { }
Downloads
- Download the content and source code used for this development kit gem. (OnScreenIndicator.zip)