External Sniper
I have finished my first good and complete mutator: the External Sniper. As promised, here is a full rundown of the code. It deals with:
- Changing the HUD,
- using UWindows,
- using configuration files and
- creating network-safe mutators.
Originally written in 2000-01-18.
Assumptions
I assume you have downloaded the mutator this tutorial is based on, and that you have read the UnrealScript reference and the Compiling tips. Have at least a glance at the code of SniperHUD, located in UnrealEd at Actor, Info, Mutator, SniperHUD.
The Tutorial
The very first thing you have to do when you want to create a mutator, is to think up a good theme for your mutator. You can’t just start programming and try some stuff out. Of course you can do that, and a lot can be learned from it. But you need to have a specific goal if you want to create a mutator that will be a success when released to the public.
This is straight from the readme text of the ExternalSniper mutator:
You’re a sniper on the red team. Zoomed in, and ready for your fifth head-shot in a row. Suddenly, your view is totally blurred by a blue haze, and before you can zoom out to actually see the bad guy standing in front of you, his flak cannon has already blown to pieces.
I guess this happened to everybody at some point. With this mod, problems like these are gone. Your sniper rifle will have a display with a nice zoomed-in view: the External Sniper Display (ESD). I saw this in a demo of the game BattleZone, and then I knew I had found a theme for a great mod.
So now you know how I got the idea for the mod, and what it does. The first question to be asked is how to start creating the desired effect.
The Abstract Function of the Mutator
First of all, forget that the mutator has to work online. A network-safe mutator is quite hard to create. It is also unnecessary to have a configuration screen at this time. It is better to focus your thoughts on the effect of the mutator first.
We have to find out how to:
- Write something to the HUD.
- Get information on the owner of the HUD, like the current weapon, position and rotation.
- Create a new viewport which will display the zoomed-in view.
- Draw the crosshair at exactly the right position.
Writing Something to the HUD
Fortunately, this has become quite simple since version 405B of Unreal
Tournament. A function called PostRender
will be called every time the screen
is updated. In this function we will do almost all the work. Here is a simple
example:
function PostRender(canvas Canvas) {
Canvas.Reset();
Canvas.SetPos(0,0);
Canvas.DrawText("This is a test");
}
The canvas is the screen itself. We can use the functions of the canvas object
to draw text, pictures, etc. The Canvas.Reset()
function will reset the
properties of the canvas to their defaults. These include the drawing color, pen
position, font etc. Canvas.SetPos(0,0)
sets the pen position to the upper left
corner. This is where the text drawn by Canvas.DrawText(...)
will appear.
Quite simple, eh?
By default mutators don’t receive PostRender calls. UT would become quite slow
if they would. To make sure our mutator does receive the calls,
RegisterHUDMutator()
has to be called when the mutator is created. I’ve done
it like this:
var bool bInitialized;
function PostBeginPlay() {
if(!bInitialized) {
RegisterHUDMutator();
bInitialized=true;
}
}
Again this is simple, once you know what function to call. PostBeginPlay()
is
called when the game has just begun. In the same fashion, PreBeginPlay()
is
called just before the game begins, and BeginPlay()
is called at the moment
the game begins. I’ve chosen to use PostBeginPlay()
, because there has to be a
player with a HUD to attach the mutator to. For some reason PostBeginPlay()
is
called twice on mutators, but one call to RegisterHUDMutator()
is enough.
That’s why I used the trick with the bInitialized
variable.
Getting the Player Information
I don’t know about a standalone game, but in a multiplayer game the owner of the
mutator is not the player who’s HUD we’re altering. With this in mind, we can’t
simply use PlayerPawn(Owner)
to retrieve information about the player.
Fortunately, every time a player spawns a call to the ModifyPlayer()
function
is made. Here is an example:
var PlayerPawn Player;
function ModifyPlayer(Pawn Other) {
if(Player==None) {
Player=PlayerPawn(Other);
}
}
function PostRender(canvas Canvas) {
// Don't display anything if we don't know the player yet.
if(Player==None) return;
Canvas.Reset();
Canvas.SetPos(0,0);
if(Player.Weapon==None)
Canvas.DrawText("You hold no weapon");
else
Canvas.DrawText("You hold a" @ Player.Weapon.MenuName);
}
This way we know the player, and we can draw some information on him/her on the
canvas. The second line in the PostRender()
function makes sure that
Player.Weapon
isn’t called on an invalid player.
Creating a Second Viewport
This one, I just have to tell you, uses a function which is NOT SUPPORTED by Epic Megagames. I’ve had only one small problem with it: it doesn’t draw things like lens flares or smoke the right way. Everyone I know who has used this function had not a single problem except this one, but still I can’t guarantee that it will work on your computer. Just to be on the safe side, I have to state this: I do not take any responsibility for damage, whatsoever, to hardware or software, directly or indirectly caused by the text of this tutorial.
Good to see you’re still with me :-)
Now it is time for the viewport to be drawn. It has to be drawn every frame, so
it has to be done in the PostRender()
function. The DrawPortal()
function
expects quite a few arguments. Those are:
DrawPortal(Left, Top, Width, Height, SomeActor, Location, Rotation, FOV);
Left
, Top
, Width
, and Height
define the position and size of the
viewport. The SomeActor
argument seems to be ignored. Just put any valid actor
in there, so the code will compile. Location
is the location the camera is
standing, and Rotation
is the direction it is looking. FOV
stands for Field
Of View, or Field Of Vision. It is half the angle (in degrees) in which you see
without moving your eyes/head. A mature human has a FOV of 90, which happens to
be the default in Unreal Tournament. The lower the FOV, the more zoomed-in the
view is.
First lets create a very simple viewport, in the upper left corner of the screen. It will be 1/16th of the size of the screen. It will be at the same location of the player, and look in the same direction. It will not be zoomed in.
function PostRender(canvas Canvas) {
Canvas.Reset();
Canvas.DrawPortal(0, 0,
Canvas.SizeX/4, Canvas.SizeY/4,
self,
Player.Location, Player.ViewRotation,
90);
}
I didn’t want the viewport to appear in the upper left corner of the screen, but
at the same position as the sniper rifle. That way it looks better, and less of
your screen is obstructed. To find the right position, we’ll have to know where
the sniper rifle will be drawn on the canvas. This position is a combination of
the default position of the rifle and the ‘Handedness’ of the player. The
Handedness
property of the player can have four values: -1, 0, 1 and 2. The
first three are for left, center and right. A handedness of 2 indicates that the
weapon should be hidden. In this case, I wanted the viewport to appear in the
center of the screen. I mean, if you want the viewport to be hidden as well,
you’re better of not using this mutator at all.
I also wanted the viewport to be scalable, so it can open and close smoothly, instead of just popping in and out. For this purpose, I calculated the center of the viewport and its size.
And for the final precision, there has to be a small alteration to the camera’s
location. The location of a player is in its center, lets say your belly button.
In the example above the camera would look from your belly button, not your
eyes. The distance between the location of the player and its eyes is stored in
the player’s EyeHeight
property.
To finish it off and have it good looking, the player’s weapon has to be moved off-screen before drawing the viewport. If you don’t do this, a very small copy of the weapon and hands are shown, which I don’t think is a pretty sight. So I just multiplied the position of the weapon by 10, drew the viewport, and divided the position by 10 again to put it back in place.
This is what the code will look like:
var PlayerPawn Player;
function PostRender(canvas Canvas) {
DrawViewport(Canvas);
// If there are more HUDMutators, pass on the PostRender call
if(NextHUDMutator!=None) {
NextHUDMutator.PostRender(Canvas);
}
}
function DrawViewport(canvas Canvas) {
local float CenterX,CenterY;
local float SizeX,SizeY;
local vector CamLocation;
local float Hand;
// If there is no player, or the player has no weapon, or
// the weapon draws its own crosshair (the sniper rifle
// does this when you zoom in), don't draw the viewport
if(Player==None || Player.Weapon==None || Player.Weapon.bOwnsCrosshair)
return;
Canvas.Reset();
CamLocation=Player.Location; // Put the camera at your belly button
CamLocation.Z+=Player.EyeHeight; // And move it up to your eyes
Hand=Player.Handedness;
if(Hand==2.0) Hand=0; // Treat a hidden weapon as a centered one
CenterX=Canvas.SizeX/2 +
class'SniperRifle'.default.PlayerViewOffset.Y*100*Hand;
CenterY=Canvas.SizeY/2 -
class'SniperRifle'.default.PlayerViewOffset.Z*50;
// The size will be 1/16th of the screen
SizeX=Canvas.SizeX/4;
SizeY=Canvas.SizeY/4;
// Move the weapon out of view
Player.Weapon.PlayerViewOffset.Y*=10;
Player.Weapon.PlayerViewOffset.Z*=10;
// Draw the viewport
Canvas.DrawPortal(CenterX-SizeX/2,CenterY-SizeY/2,
SizeX,SizeY,
self,
CamLocation,Player.ViewRotation,
8);
// Return the weapon to its original position
Player.Weapon.PlayerViewOffset.Y/=10;
Player.Weapon.PlayerViewOffset.Z/=10;
}
Drawing the Crosshair
Drawing the crosshair actually consists of two steps: calculating the exact position of the crosshair, and the actual drawing. The position has to be 100% accurate, otherwise you’re better off without.
My guess was that the crosshair on your “main viewport” is as accurate as it gets, so why not copy Epic’s crosshair-code and adopt it to our needs?
To keep the size of this tutorial within bounds, I haven’t included the code. Everybody interested in this subject can look it up from UnrealEd. I recommend you print the DrawCrosshair function, or put it on your second monitor. If you do not have a second monitor attached to your computer, it is an absolute must if you want to set up a relaxed programming environment! grin
In the original ChallengeHUD (the generic HUD used by UT), the crosshair is
scaled to become a little larger when you pick something up. I found this
unnecessary, annoying even, to implement this in the sniper viewport, so I
removed the calculation of the XScale
variable and set it to a fixed value:
0.75.
The original DrawCrosshair()
function was part of a HUD object, but ours
isn’t. Therefore we have to convert the Player.MyHUD
variable to a
ChallengeHUD and use this to access the HUDs properties, like the type of
crosshair. Just to be sure, if Player.MyHUD
can’t be converted to a
ChallengeHUD, skip drawing the crosshair. I always had a crosshair on my sniper
viewport, but you never know what someone might try.
This resulted in the following additions/changes:
local ChallengeHUD Hud;
Hud=ChallengeHUD(Player.MyHUD);
if(Hud==None) return;
// this was: if(Crosshair>8) return;
// but Crosshair is a property of the HUD.
if(Hud.Crosshair>8) return;
( some other, mostly unchanged code. Just prepend Hud. to:
PlayerOwner.Handedness, CrosshairColor, CrossHairTextures,
Crosshair and LoadCrosshair )
// This actually draws the crosshair
// T is the texture, XLength is the width (and the height, because it
// is square, and 0,0,64,64 are the position > size of the square
// on the texture to draw.
Canvas.DrawTile(T, XLength, XLength, 0, 0, 64, 64);
Well, now we’ve created a mutator that draws the viewport at the right position, and with a very precise crosshair. I haven’t provided you with the full code, but I’ve given enough information so that you can create it.
What has to be done now, is to modify it in such a way that it works in networked games as well. And we have to add some extra configuration possibilities and eye-candy.
Creating a Network Safe Mutator
Above we fetched the player information in the ModifyPlayer
function call.
Unfortunately, this won’t work in a networked game. First, I’ll try to explain a
little about the client-server structure.
To be able to play through the internet (or any network for that matter), as little information as possible has to be sent between the server and the clients. For instance, a player running towards you, emptying a flak cannon into your face, will be sent to you (assuming you are a client who has joined the server). Information about a heath pack on the other side of the level won’t be sent to you, because there is no way that information is useful to your computer. This sending of information is called replication.
Since the layout of your HUD is totally uninteresting to the server, the HUD is created on your computer, and not on the server, and isn’t replicated to the server. The only HUD known to the server, is the HUD of the player who is serving the game. Of course a dedicated server doesn’t know any information on any HUD.
The mutator code runs on the server, except for specially marked functions.
Those functions are called simulated
functions. This is why the PostRender()
function in my mutator is marked as simulated
: it has to draw something on the
client’s HUD.
As you can see in the mutator code, the ModifyPlayer
function isn’t a
simulated one. So we can’t use Player.MyHUD
there, because the information on
the HUD is stored on the client, not on the server.
You can however use a SpawnNotify
object. I’ll first give an example code, and
then explain how it works. The SniperHUD
class is the HUDMutator, not the HUD
itself!
class SniperNotify expands SpawnNotify;
simulated function SpawnNewHUD(Actor A) {
local SniperHUD NewHUDMut;
// Spawn a new sniperHUD
NewHUDMut=spawn(class'ExternSniper.SniperHUD',A);
// We're on the client side now, so all player info is known!
NewHUDMut.Player=PlayerPawn(A.Owner);
// This causes our newly created HUDMutator
// to receive PostRender calls
NewHUDMut.RegisterHUDMutator();
}
// This function is called on the client side (!) whenever
// an object of type 'ActorClass' (defined below) is spawned
simulated event Actor SpawnNotification( Actor A ) {
// If, for any strange reason, the spawned object A
// is a SniperHUD, we won't spawn a new one.
if(!A.IsA('SniperHUD')) SpawnNewHUD(A);
return A;
}
defaultproperties
{
ActorClass=class'HUD'
}
The SpawnNotify object is programmed in such a fashion that it replicates itself to every player in the game, even if the player joins when the game has long started.
The SpawnNotification()
is called on the client side whenever a HUD is
spawned. The owner of the HUD is the player, so we can use PlayerPawn(A.Owner)
to set the Player
property of the SniperHUD object and to register our mutator
as a HUDMutator
Now we know how to attach our mutator to the HUD, we still have to spawn the
SpawnNotify
object. This will be done in another expansion of the Mutator
class. This will be the mutator you’ll select from the menu!
Here is the code, it is quite straightforward:
class ExternSniper expands Mutator;
var bool bInitialized;
simulated function PreBeginPlay() {
if(!bInitialized) {
spawn(class'ExternalSniper.SniperNotify');
bInitialized=true;
}
}
To finish off the networking problem, we have to alter some of the code of the
SniperHUD
class.
- The entire
ModifyPlayer
andPostBeginPlay
functions can be removed, since their functionality has been copied to theSniperNotify
object. - The
PostRender
andDrawViewport
functions have to besimulated
so they will be called on the client.
Adding some Eye-candy
There are three things I want to change to the appearance of the sniper viewport:
- When changing from a visible to a invisible state, I want some sort of animation, like starting like a 1 pixel high horizontal line and then smoothly growing to the wanted size.
- The sniper viewport should be only visible if you’re using the sniper rifle
- Add a nice border around the viewport.
Animating the sniper viewport
An animation depends on the flow of time. Since we don’t know how many times per
second the PostRender
function is called, we’ll have to use another function.
This one is called Tick
, and has one parameter: DeltaTime
, the time that has
passed since the last call to Tick
. This is exactly what we want! Like
PostRender
, Tick
is called as often as possible. Tick
is an event instead
of a function. Those are exactly the same to us, the difference is interesting
to people who want to combine C++ DLLs with UnrealScript.
In the Tick
function, we will modify two variables: DestFactor
and Factor
.
DestFactor
is an integer, which will hold the requested state of the viewport.
It is either 0 (closed) or 1 (open). Factor
is a float, which will be the
current size of the viewport. 1 will mean it is entirely opened, 0.5 will be
half-opened, etc. Every tick the Factor
will be increased or decreased by
DeltaTime
, depending on the current value of DestFactor
and Factor
.
DestFactor
depends on the weapon the player is holding.
var float Factor;
function Tick(float DeltaTime) {
local int DestFactor;
Super.Tick(DeltaTime);
if(Player.Weapon!=None && Player.Weapon.IsA('SniperRifle'))
DestFactor=1;
else
DestFactor=0;
if(Factor<DestFactor) Factor+=DeltaTime;
else if(Factor>DestFactor) Factor-=DeltaTime;
if(Factor<0) Factor=0;
if(Factor>1) Factor=1;
}
All that is left to creating the animation, is to make PostRender
dependant on
Factor
. We can do that by editing the DrawViewport
function. Replace
SizeY=Canvas.SizeY/4;
with
SizeY=Canvas.SizeY/4*Factor;
Et voilá! A nice smooth animation is born :-)
Making the Viewport Visible Only if You Use the Sniper Rifle
If you have read code from the section above, you’ll see that DestFactor
will
be only 1 if the player has a sniper rifle. So this has been taken care of!
Adding a Nice Border to The Viewport
I thought the viewport on its own was a little nude. It was just floating there in mid air. Of course, adding a two pixel thick border doesn’t attach it to something, but it sure is a much nicer sight.
First of all, create a texture. I used this one, which I created myself. It is 64x64 in size, 256 colors, grayscaled:
To let UT know we want to use this texture, I use this approach:
- Put the texture in
UnrealTournament\ExternSniper\Models\SniperBorder.pcx
- Add this line to the .uc file, just after the
class
command:#exec TEXTURE IMPORT NAME=SniperBorder FILE=Models\SniperBorder.pcx Group="Borders"
. - Compile your package using
ucc make
.
For drawing the border, I used the same function as for drawing the crosshair.
You have to draw the border before the viewport is called, otherwise the
border texture will be drawn on top of it. To draw, add those two lines to your
DrawViewport
:
Canvas.SetPos(CenterX-SizeX/2-2,CenterY-SizeY/2-2);
Canvas.DrawTile(Texture'SniperBorder',SizeX+4,SizeY+4,0,0,64,64);
Creating a Configuration Display
I haven’t finished this yet. If you want to know more about UWindows, I recommend you read the Squidi’s UWindows tutorials at the Mutation Device
Saving Your Configuration in a .Ini File
This one is really simple. Just add config(FileName)
to the class ClassName expands ParentClass
line of your code to tell UT the file you’ll be using. Add
the config
keyword to each variable you want to safe. Here is an example:
class MyActor expands Actor config(MyINIFile);
var() config boolbSomeProperty;
defaultproperties {
bSomeProperty=false;
}
That’s It!
Well, that’s it! You’re now equipped with enough knowledge to create your own mutators. If anything in this tutorial is unclear to you, please contact me.