Adding a Scope
The code and ideas presented in this tutorial can be used to add a HUD-based sniper scope to your game, for use by any weapon that you see fit. As an example, it has been added alongside the existing zoom functionality of the HL2 crossbow. However, it is possible to tell it to display from any part of your code.
Contents
Prerequisites
The only prerequisite for this tutorial is that you have already created a sniper scope texture (e.g. a big black square/rectangle with a transparent circle in the middle of it) which has an appropriate .VMT file.
This material must be setup to be loaded as a HUD texture. Do this by adding the following to scripts/hud_textures.txt
:
sprites/640_hud
{
TextureData
{
scope
{
file scope
x 0
y 0
width 512
height 512
}
...
}
}
The HUD element
This element displays your scope texture on the HUD. Just add the following code to hud_scope.cpp
, which you should create somewhere in your game's client project.
#include "cbase.h"
#include "hudelement.h"
#include "hud_macros.h"
#include "iclientmode.h"
#include "c_basehlplayer.h" //alternative #include "c_baseplayer.h"
#include <vgui/IScheme.h>
#include <vgui_controls/Panel.h>
// memdbgon must be the last include file in a .cpp file!
#include "tier0/memdbgon.h"
/**
* Simple HUD element for displaying a sniper scope on screen
*/
class CHudScope : public vgui::Panel, public CHudElement
{
DECLARE_CLASS_SIMPLE( CHudScope, vgui::Panel );
public:
CHudScope( const char *pElementName );
void Init();
void MsgFunc_ShowScope( bf_read &msg );
protected:
virtual void ApplySchemeSettings(vgui::IScheme *scheme);
virtual void Paint( void );
private:
bool m_bShow;
CHudTexture* m_pScope;
};
DECLARE_HUDELEMENT( CHudScope );
DECLARE_HUD_MESSAGE( CHudScope, ShowScope );
using namespace vgui;
/**
* Constructor - generic HUD element initialization stuff. Make sure our 2 member variables
* are instantiated.
*/
CHudScope::CHudScope( const char *pElementName ) : CHudElement(pElementName), BaseClass(NULL, "HudScope")
{
vgui::Panel *pParent = g_pClientMode->GetViewport();
SetParent( pParent );
m_bShow = false;
m_pScope = 0;
// Scope will not show when the player is dead
SetHiddenBits( HIDEHUD_PLAYERDEAD );
// fix for users with diffrent screen ratio (Lodle)
int screenWide, screenTall;
GetHudSize(screenWide, screenTall);
SetBounds(0, 0, screenWide, screenTall);
}
/**
* Hook up our HUD message, and make sure we are not showing the scope
*/
void CHudScope::Init()
{
HOOK_HUD_MESSAGE( CHudScope, ShowScope );
m_bShow = false;
}
/**
* Load in the scope material here
*/
void CHudScope::ApplySchemeSettings( vgui::IScheme *scheme )
{
BaseClass::ApplySchemeSettings(scheme);
SetPaintBackgroundEnabled(false);
SetPaintBorderEnabled(false);
if (!m_pScope)
{
m_pScope = gHUD.GetIcon("scope");
}
}
/**
* Simple - if we want to show the scope, draw it. Otherwise don't.
*/
void CHudScope::Paint( void )
{
C_BasePlayer* pPlayer = C_BasePlayer::GetLocalPlayer();
if (!pPlayer)
{
return;
}
if (m_bShow)
{
//Perform depth hack to prevent clips by world
//materials->DepthRange( 0.0f, 0.1f );
// This will draw the scope at the origin of this HUD element, and
// stretch it to the width and height of the element. As long as the
// HUD element is set up to cover the entire screen, so will the scope
m_pScope->DrawSelf(0, 0, GetWide(), GetTall(), Color(255,255,255,255));
//Restore depth
//materials->DepthRange( 0.0f, 1.0f );
// Hide the crosshair
pPlayer->m_Local.m_iHideHUD |= HIDEHUD_CROSSHAIR;
}
else if ((pPlayer->m_Local.m_iHideHUD & HIDEHUD_CROSSHAIR) != 0)
{
pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_CROSSHAIR;
}
}
/**
* Callback for our message - set the show variable to whatever
* boolean value is received in the message
*/
void CHudScope::MsgFunc_ShowScope(bf_read &msg)
{
m_bShow = msg.ReadByte();
}
This will make the element cover the entire screen.
Telling the HUD element when to show
For this, a HUD (aka User) message is used, which is called ShowScope
. To enable this, add the following line to your user messages registration code (e.g. hl2_usermessages.cpp
or similar), in the RegisterUserMessages()
function:
usermessages->Register( "ShowScope", 1); // show the sniper scope
Then all you need to do is fire off this message with either a true or false value (1 or 0) to show or hide the scope. For example, change the CWeaponCrossbow::ToggleZoom()
function to the following:
void CWeaponCrossbow::ToggleZoom( void )
{
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if ( pPlayer == NULL )
return;
#ifndef CLIENT_DLL
if ( m_bInZoom )
{
if ( pPlayer->SetFOV( this, 0, 0.2f ) )
{
m_bInZoom = false;
// Send a message to hide the scope
CSingleUserRecipientFilter filter(pPlayer);
UserMessageBegin(filter, "ShowScope");
WRITE_BYTE(0);
MessageEnd();
}
}
else
{
if ( pPlayer->SetFOV( this, 20, 0.1f ) )
{
m_bInZoom = true;
// Send a message to Show the scope
CSingleUserRecipientFilter filter(pPlayer);
UserMessageBegin(filter, "ShowScope");
WRITE_BYTE(1);
MessageEnd();
}
}
#endif
}
Making a weapon zoom
This section deals with actually adding zoom functionality to a weapon in the first place. Here's what you need to do - most of this code is lifted straight from CWeaponCrossbow
. For the purpose of this tutorial, zoom is being added to the .357 Magnum as a secondary fire (like the crossbow). If you want to implement it in a weapon which already uses the IN_ATTACK2
key to perform a secondary attack, you'll need to define and use a separate key for this.
(Note: This section has not been updated and/or cleaned up, so if you get errors when following this code please update this page with fixes you find as has been done by me in the other sections - TymaxBeta)
To a very basic weapon like the .357, you need to add one boolean field and 5 new functions to achieve the zoom function like the crossbow. Bear in mind that many weapons (like the pistol) already define one or more of these functions, and all you need to do is add to it.
public:
bool Holster( CBaseCombatWeapon *pSwitchingTo = NULL ); // Required so that you know to un-zoom when switching weapons
void ItemPostFrame( void ); // Called every frame during normal weapon idle
void ItemBusyFrame( void ); // Called when the weapon is 'busy' e.g. reloading
protected:
void ToggleZoom( void ); // If the weapon is zoomed, un-zoom and vice versa
void CheckZoomToggle( void ); // Check if the secondary attack button has been pressed
bool m_bInZoom; // Set to true when you are zooming, false when not
You would then add the following definitions for those functions (again, just add the relevant code to functions if they already exist):
/**
* Check for weapon being holstered so we can disable scope zoom
*/
bool CWeapon357::Holster(CBaseCombatWeapon *pSwitchingTo /* = NULL */)
{
if ( m_bInZoom )
{
ToggleZoom();
}
return BaseClass::Holster( pSwitchingTo );
}
/**
* Check the status of the zoom key every frame to see if player is still zoomed in
*/
void CWeapon357::ItemPostFrame()
{
// Allow zoom toggling
CheckZoomToggle();
BaseClass::ItemPostFrame();
}
/**
* Check the status of the zoom key every frame to see if player is still zoomed in
*/
void CWeapon357::ItemBusyFrame( void )
{
// Allow zoom toggling even when we're reloading
CheckZoomToggle();
BaseClass::ItemBusyFrame();
}
/**
* Check if the zoom key was pressed in the last input tick
*/
void CWeapon357::CheckZoomToggle( void )
{
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if ( pPlayer && (pPlayer->m_afButtonPressed & IN_ATTACK2)) //We need to include "in_buttons.h" for IN_ATTACK2
{
ToggleZoom();
}
}
/**
* If we're zooming, stop. If we're not, start.
*/
void CWeapon357::ToggleZoom( void )
{
CBasePlayer *pPlayer = ToBasePlayer( GetOwner() );
if ( pPlayer == NULL )
return;
#ifndef CLIENT_DLL
if ( m_bInZoom )
{
// Narrowing the Field Of View here is what gives us the zoomed effect
if ( pPlayer->SetFOV( this, 0, 0.2f ) )
{
m_bInZoom = false;
// Send a message to hide the scope
CSingleUserRecipientFilter filter(pPlayer);
UserMessageBegin(filter, "ShowScope");
WRITE_BYTE(0);
MessageEnd();
}
}
else
{
if ( pPlayer->SetFOV( this, 20, 0.1f ) )
{
m_bInZoom = true;
// Send a message to Show the scope
CSingleUserRecipientFilter filter(pPlayer);
UserMessageBegin(filter, "ShowScope");
WRITE_BYTE(1);
MessageEnd();
}
}
#endif
}
Alternatives
An alternative way to add a scope (in HL2) is to allow firing while zooming. Whilst it applies to all weapons, it is far easier to implement.
In HL2_Player.cpp
comment out the following block of code (line 523):
if ( m_nButtons & IN_ZOOM )
{
//FIXME: Held weapons like the grenade get sad when this happens
m_nButtons &= ~(IN_ATTACK|IN_ATTACK2);
}
Note that this can also be easily accomplished through the in-game console (in base-HL2) by simply binding a key to "toggle_zoom" (i.e. bind mouse3 toggle_zoom). Zooming does not have the same restriction on firing when it's toggled instead of held.
Different screen ratios
Scopes go funky when used on different screen ratios. Here are two methods for a fix, for method 2 you will need to re-make the scope for each ratio (4:3, 16:9 and 16:10).
If you don't use one of these fixes people with different ratios will see a stretched scope and have an unfair advantage.
Method 1
Find: m_pScope->DrawSelf(0, 0, GetWide(), GetTall(), Color(255,255,255,255));
in hud_scope.cpp that was created before, and replace it with:
int x1 = (GetWide() / 2) - (GetTall() / 2);
int x2 = GetWide() - (x1 * 2);
int x3 = GetWide() - x1;
surface()->DrawSetColor(Color(0,0,0,255));
surface()->DrawFilledRect(0, 0, x1, GetTall()); //Fill in the left side
surface()->DrawSetColor(Color(0,0,0,255));
surface()->DrawFilledRect(x3, 0, GetWide(), GetTall()); //Fill in the right side
m_pScope->DrawSelf(x1, 0, x2, GetTall(), Color(255,255,255,255)); //Draw the scope as a perfect square
--BakonGuy 04:07, 14 November 2010 (UTC)
Remember To add #include <vgui/ISurface.h> otherwise Visual Studio will complain about decorations. Lukeme99 09:44, 22 April 2012 (PDT)
Method 2
Add to the declaration of the scope under private:
char* scope_src[3];
int m_iRes;
Under CHudScope::CHudScope
add:
scope_src[0] = "scope1640";
scope_src[1] = "scope11280";
scope_src[2] = "scope1720";
This is an array which lists the scope names for the different screen ratio (in hud_textures.res
). Do not add underscores (_) to your names otherwise it causes a freak error and crash your game.
Change ApplySchemeSettings()
to:
void CHudScope::ApplySchemeSettings( vgui::IScheme *scheme )
{
BaseClass::ApplySchemeSettings(scheme);
SetPaintBackgroundEnabled(false);
SetPaintBorderEnabled(false);
}
Change Paint()
to:
void CHudScope::Paint( void )
{
C_BasePlayer* pPlayer = C_BasePlayer::GetLocalPlayer();
if (!pPlayer)
{
return;
}
if (m_bShow)
{
vgui::Panel *pParent = g_pClientMode->GetViewport();
int screenWide=pParent->GetWide();
int screenTall=pParent->GetTall();
float ratio = ((float)screenWide)/((float)screenTall);
if (ratio >= 1.59 && ratio <= 1.61 )
{
m_iRes = 2;
}
else if (ratio >= 1.76 && ratio <= 1.78 )
{
m_iRes = 1;
}
else
{
m_iRes = 0;
}
m_pScope = gHUD.GetIcon( scope_src[ m_iRes ] );
if (m_pScope)
{
//Performing depth hack to prevent clips by world
materials->DepthRange( 0.0f, 0.1f );
m_pScope->DrawSelf(0, 0, pParent->GetWide(), pParent->GetTall(), Color(255,255,255,255));
//Restore depth
materials->DepthRange( 0.0f, 1.0f );
// Hide the crosshair
pPlayer->m_Local.m_iHideHUD |= HIDEHUD_CROSSHAIR;
}
}
else if ((pPlayer->m_Local.m_iHideHUD & HIDEHUD_CROSSHAIR) != 0)
{
pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_CROSSHAIR;
}
}
Also, add this to hud_texture.txt
for the scope textures:
scope1640
{
file HUD\scopes\scope_1_640_480
x 0
y 0
width 1024
height 512
}
scope11280
{
file HUD\scopes\scope_1_1280_720
x 0
y 0
width 1024
height 512
}
scope1720
{
file HUD\scopes\scope_1_720_480
x 0
y 0
width 1024
height 512
}
Multiple scopes
It's quite easy to add multiple scopes if you use the code for different screen ratios above.
Add this to the declarations:
int m_iType;
Under CHudScope::CHudScope
add in your extra scopes to the array. For example:
scope_src[0] = scope1640;
scope_src[1] = scope11280;
scope_src[2] = scope1720;
scope_src[3] = scope2640;
scope_src[4] = scope21280;
scope_src[5] = scope2720;
scope_src[6] = scope3640;
scope_src[7] = scope31280;
scope_src[8] = scope3720;
In Paint()
change:
m_pScope = gHUD.GetIcon( scope_src[ m_iRes ] );
to
m_pScope = gHUD.GetIcon( scope_src[ 3*(m_iType - 1)+m_iRes ] );
change MsgFunc_ShowScope() to:
void CHudScope::MsgFunc_ShowScope(bf_read &msg)
{
m_iType = msg.ReadByte();
if (m_iType >= 1)
{
m_bShow = true;
}
else
{
m_bShow = false;
}
}
Now to change the scope type by weapon (or how ever you call it) just change the byte it sends in the msg.
WRITE_BYTE(0); // no scope
WRITE_BYTE(1); // scope 1
WRITE_BYTE(2); // scope 2
WRITE_BYTE(3); // scope 3
Also add this to hud_texture.txt
for the scope textures:
scope1640
{
file HUD\scopes\scope_1_640_480
x 0
y 0
width 1024
height 512
}
scope11280
{
file HUD\scopes\scope_1_1280_720
x 0
y 0
width 1024
height 512
}
scope1720
{
file HUD\scopes\scope_1_720_480
x 0
y 0
width 1024
height 512
}
scope2640
{
file HUD\scopes\scope_2_640_480
x 0
y 0
width 1024
height 512
}
scope21280
{
file HUD\scopes\scope_2_1280_720
x 0
y 0
width 1024
height 512
}
scope2720
{
file HUD\scopes\scope_2_720_480
x 0
y 0
width 1024
height 512
}
scope3640
{
file HUD\scopes\scope_3_640_480
x 0
y 0
width 1024
height 512
}
scope31280
{
file HUD\scopes\scope_3_1280_720
x 0
y 0
width 1024
height 512
}
scope3720
{
file HUD\scopes\scope_3_720_480
x 0
y 0
width 1024
height 512
}
Using 4-Part Valve Scope Textures
If you look into how Valve implemented their scopes for Counter-Strike: Source and Day of Defeat: Source, you'll find that they use a 256x256 texture for each corner of the scope. In Day of Defeat: Source these are four different 256x256 textures that are stretched to a 4:3 aspect ratio. In this example I will show you how to implement the Kar98's scope from Day of Defeat: Source assuming you have either extracted the scope texture to the same relative path within your mod folder or are mounting the game's content through your gameinfo.txt file.
The first step is to add the texture for the scope to your hud_textures.txt file as shown in this article:
"scopell"
{
"file" "sprites/scopes/scope_k43_ll"
"x" "0"
"y" "0"
"width" "256"
"height" "256"
}
"scopelr"
{
"file" "sprites/scopes/scope_k43_lr"
"x" "0"
"y" "0"
"width" "256"
"height" "256"
}
"scopeul"
{
"file" "sprites/scopes/scope_k43_ul"
"x" "0"
"y" "0"
"width" "256"
"height" "256"
}
"scopeur"
{
"file" "sprites/scopes/scope_k43_ur"
"x" "0"
"y" "0"
"width" "256"
"height" "256"
}
From here on I will be showing code as if you are only using a single scope for your game. If you wish to use multiple, you will need to convert each corner to an array. You could also put all four corners into an array as well for just one scope to tidy things up a bit.
For the code, we will first go to the private variables of the CHudScope class and replace m_pScope with one variable for each corner:
CHudTexture* m_pScopell;
CHudTexture* m_pScopelr;
CHudTexture* m_pScopeul;
CHudTexture* m_pScopeur;
Next in the constructor, CHudScope::CHudScope, we will do the same; removing m_pScope and initializing all the variables to 0.
m_pScopell = 0;
m_pScopelr = 0;
m_pScopeul = 0;
m_pScopeur = 0;
Again in ApplySchemeSettings:
if (!m_pScopell)
{
m_pScopell = gHUD.GetIcon("scopell");
}
if (!m_pScopelr)
{
m_pScopelr = gHUD.GetIcon("scopelr");
}
if (!m_pScopeul)
{
m_pScopeul = gHUD.GetIcon("scopeul");
}
if (!m_pScopeur)
{
m_pScopeur = gHUD.GetIcon("scopeur");
}
Finally we are are actually going to paint the scope. This will be using method 1 for dealing with multiple aspect ratios. We cannot just replace the previous code with four different blocks as the result will give us a square and the scopes from Day of Defeat: Source are designed to be stretched to 4:3. If we consider the width of the screen to be 'w' and the height 'h', we can do some simple math to determine exactly how large the black bars should be on each side of the scope as well as the exact width and height of each scope block.
Under Paint() we can replace the previous code with this:
void CHudScope::Paint(void)
{
C_BasePlayer* pPlayer = C_BasePlayer::GetLocalPlayer();
if (!pPlayer)
{
return;
}
if (m_bShow)
{
int w = GetWide(); // Width of player's screen resolution.
int h = GetTall(); // Height of player's screen resolution.
// Our scope's width must be 4/3 the height of the screen as that is how Valve designed the textures.
// This means that one quadrant will have a width exactly half of that, 2/3 the height of the screen.
int x = 2 * h / 3;
int y = h / 2; // Our scope blocks will each be half the height of the screen.
int margin = (w - 4 * h / 3) / 2; // This is the width of the black bars on the sides of the screen.
surface()->DrawSetColor(Color(0, 0, 0, 255));
surface()->DrawFilledRect(0, 0, margin, h); //Fill in the left side
surface()->DrawSetColor(Color(0, 0, 0, 255));
surface()->DrawFilledRect(margin + 2 * x, 0, w, h); //Fill in the right side, it's offset horizontally by the width of one margin plus two scope block widths.
// Source engine displays UI elements with an inverted-y axis.
// This means that the point (0,0) is located in the upper left corner of the screen and is why our lower scope blocks must be offset by y = h / 2.
m_pScopell->DrawSelf(margin, y, x, y, Color(255, 255, 255, 255));
m_pScopeul->DrawSelf(margin, 0, x, y, Color(255, 255, 255, 255));
m_pScopelr->DrawSelf(margin + x, y, x, y, Color(255, 255, 255, 255));
m_pScopeur->DrawSelf(margin + x, 0, x, y, Color(255, 255, 255, 255));
pPlayer->m_Local.m_iHideHUD |= HIDEHUD_CROSSHAIR; // This hides the crosshair while the scope is active.
}
else if ((pPlayer->m_Local.m_iHideHUD & HIDEHUD_CROSSHAIR) != 0)
{
pPlayer->m_Local.m_iHideHUD &= ~HIDEHUD_CROSSHAIR;
}
}