UT tutorial: 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.

© by Sybren Stüvel, written on 18 jan 2000.

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 right 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:

  1. Write something to the HUD.
  2. Get information on the owner of the HUD, like the current weapon, position and rotation.
  3. Create a new viewport which will display the zoomed-in view.
  4. 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 and PostBeginPlay functions can be removed, since their functionality has been copied to the SniperNotify object.
  • The PostRender and DrawViewport functions have to be simulated, 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:

  1. 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.
  2. The sniper viewport should be only visible if you're using the sniper rifle
  3. 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:

A nice border

To let UT know we want to use this texture, I use this approach:

  1. Put the texture in UnrealTournament\ ExternSniper\ Models\ SniperBorder.pcx
  2. Add this line to the .uc file, just after the class command: #exec TEXTURE IMPORT NAME=SniperBorder FILE=Models\SniperBorder.pcx Group="Borders".
  3. 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.