World Hints

World hints are snippets of information in the level that help the user navigate or progress through the game. The one's I added to Resurgence used a specific font and had three sizes, they where placed in the level using a hammer entity and always faced the player which is called billboarding.

So to implement this in your very own mod you first need to add the worldhint.cpp file for the server-side entity that's setup by hammer so that the level designer can create a worldhint entity with a font size, hint scale and the text for the hint itself.

#include "cbase.h"

class CWorldHint : public CBaseEntity
{
public:
    DECLARE_CLASS( CWorldHint, CBaseEntity );
    DECLARE_SERVERCLASS();

    CWorldHint();
    void Spawn();
    void Activate();

    //Always send to all clients
    int UpdateTransmitState()
    {
        return SetTransmitState( FL_EDICT_ALWAYS );
    }

private:
    CNetworkString( hint, MAX_PATH );
    CNetworkVar( int, fontSize );
    CNetworkVar( float, scalar );

    string_t m_String_tHint;

protected:
    DECLARE_DATADESC();
};

LINK_ENTITY_TO_CLASS( worldhint, CWorldHint )

BEGIN_DATADESC( CWorldHint )
    DEFINE_KEYFIELD( m_String_tHint, FIELD_STRING, "Hint" ),
    DEFINE_KEYFIELD( fontSize, FIELD_INTEGER, "Size" ),
    DEFINE_KEYFIELD( scalar, FIELD_FLOAT, "Scalar" )
END_DATADESC()

IMPLEMENT_SERVERCLASS_ST( CWorldHint, DT_WorldHint )
    SendPropString( SENDINFO( hint ) ),
    SendPropInt( SENDINFO( fontSize ) ),
    SendPropFloat( SENDINFO( scalar ) ),
END_SEND_TABLE()

CWorldHint::CWorldHint()
{
    fontSize = 1;
    memset( hint.GetForModify(), 0, sizeof( hint ) );
}

void CWorldHint::Spawn()
{
    SetModelName( MAKE_STRING( "worldhint" ) );
    SetSolid( SOLID_NONE );
    SetMoveType( MOVETYPE_NONE );

    Precache();
    SetModel( STRING( GetModelName() ) );

    AddEffects( EF_NOSHADOW | EF_NORECEIVESHADOW );
}

void CWorldHint::Activate()
{
    BaseClass::Activate();
    Q_strncpy( hint.GetForModify(), STRING( m_String_tHint ), 255 );
}

This is a pretty basic entity setup, the only special thing it does is copy the hint text from hammer to the variable used to send the info over to the client. It also completely hides itself as it's clientside only. This following server-side code is only 200 lines long and does the rest, first it creates a VGUI panel then sets up correct angles and world position and then proceeds to render the VGUI element in the world using the magic function called DrawPanelIn3DSpace.

#include "cbase.h"
#include "fmtstr.h"

//Vgui render stuff
#include "VGuiMatSurface/IMatSystemSurface.h"
#include <vgui_controls/Panel.h>
#include <vgui_controls/Label.h>
#include <vgui/ISurface.h>

//Debugging
#include "debugoverlay_shared.h"

//
//    Special hint panel used for drawing
//
class HintPanel : public vgui::Panel
{
public:
    HintPanel( const char *text, int fontSize ) : fontSize( fontSize )
    {
        SetVisible( true );
        SetPos( 0, 0 );

        //Create basic label
        label = new vgui::Label( this, "", text );
        label->SetPos( 0, 0 );
    }

    void ApplySchemeSettings( vgui::IScheme *pScheme )
    {
        //Get the font and set the label
        vgui::HFont textFont = pScheme->GetFont( CFmtStr( "WorldHint%i", fontSize ) );
        label->SetFont( textFont );

        //Get the labels text in wchar format
        wchar_t wText[256];
        label->GetText( wText, 256 );

        //Calculate the texts size
        int w, h;
        vgui::surface()->GetTextSize( textFont, wText, w, h );

        //Set the label and panels size to the texts size
        label->SetSize( w, h );
        SetSize( w, h );
    }

private:
    vgui::Label *label;
    int fontSize;
};

ConVar res_debug_worldhint( "res_debug_worldhint", "0" );
ConVar res_worldhint( "res_worldhint", "1" );

//
//    Client-side worldhint entity for rendering the actual hint
//
class C_WorldHint : public C_BaseEntity
{
public:
    DECLARE_CLASS( C_WorldHint, C_BaseEntity );
    DECLARE_CLIENTCLASS();

    C_WorldHint()
    {
        hintPanel = NULL;
    }

    virtual int DrawModel( int flags );
    virtual void OnDataChanged( DataUpdateType_t type );

    virtual void GetRenderBounds( Vector &vecMins, Vector &vecMaxs )
    {
        //Calculate the bounds for the hint
        Vector half = Vector( width/2.0f, width/2.0f, height/2.0f );
        vecMins.Init( -half.x, -half.y, -half.z );
        vecMaxs.Init( half.x, half.y, half.z );

        //Debug the bounds
        if( res_debug_worldhint.GetBool() )
            NDebugOverlay::Box( GetAbsOrigin(), vecMins, vecMaxs, 255, 0, 0, 128, 1.0f );
    }

    //Matrix stuff
    void ComputePanelToWorld();
    Vector CalculateOrigin( QAngle angles );
    QAngle CalculateAngle();

private:
    char hint[MAX_PATH];
    int fontSize;
    float scalar;
    VMatrix m_PanelToWorld;

    //Panel stuff
    float width, height;
    int pWidth, pHeight;
    HintPanel *hintPanel;
};

LINK_ENTITY_TO_CLASS( worldhint, C_WorldHint );

IMPLEMENT_CLIENTCLASS_DT( C_WorldHint, DT_WorldHint, CWorldHint )
    RecvPropString( RECVINFO( hint ) ),
    RecvPropInt( RECVINFO( fontSize ) ),
    RecvPropFloat( RECVINFO( scalar ) ),
END_RECV_TABLE()

void C_WorldHint::OnDataChanged( DataUpdateType_t type )
{
    //We have some data to use
    if( type == DATA_UPDATE_CREATED )
    {
        //Create the hint panel with our data
        hintPanel = new HintPanel( hint, fontSize );
        hintPanel->InvalidateLayout( true, true );//Setup scheme stuff
        hintPanel->GetSize( pWidth, pHeight );

        //
        width = (float)pWidth/scalar;
        height = (float)pHeight/scalar;
    }

    BaseClass::OnDataChanged( type );
}

Vector C_WorldHint::CalculateOrigin( QAngle angles )
{
    Vector vecOrigin = GetAbsOrigin();

    //Get the angle vectors
    Vector xaxis, yaxis;
    AngleVectors( angles, &xaxis, &yaxis, NULL );

    //Transform the x and y axis to center it
    VectorMA( vecOrigin, -(width/2), xaxis, vecOrigin );
    VectorMA( vecOrigin, -(height/2), yaxis, vecOrigin );

    //Return new origin
    return vecOrigin;
}

QAngle C_WorldHint::CalculateAngle()
{
    //Get the player
    C_BasePlayer *pLocalPlayer = C_BasePlayer::GetLocalPlayer();
    if( !pLocalPlayer )
        return vec3_angle;

    //Get the direction between this and the player
    Vector target = GetAbsOrigin()-(pLocalPlayer->GetAbsOrigin()+Vector(0,0,pLocalPlayer->GetPlayerMaxs().z/2));

    //Convert to angles
    QAngle angles;
    VectorAngles( target, angles );

    //Fix angles
    angles[YAW] += 90;
    angles[ROLL] = (-angles[PITCH])-90;
    angles[PITCH] = 180;

    //Return angles
    return angles;
}

void C_WorldHint::ComputePanelToWorld()
{
    //Get out angle and origin
    QAngle angles = CalculateAngle();
    Vector origin = CalculateOrigin( angles );

    //Set the matrix to them
    m_PanelToWorld.SetupMatrixOrgAngles( origin, angles );

    //Debug
    if( res_debug_worldhint.GetBool() )
    {
        NDebugOverlay::Box( GetAbsOrigin(), Vector( -2,-2,-2 ), Vector( 2, 2, 2 ), 0, 255, 0, 255, 0.01f );
        NDebugOverlay::Box( origin, Vector( -2,-2,-2 ), Vector( 2, 2, 2 ), 255, 0, 0, 255, 0.01f );
    }
}

int C_WorldHint::DrawModel( int flags )
{
    //Client doesn't want to render these
    if( !res_worldhint.GetBool() )
        return 0;

    //Wait for a panel to draw
    if( !hintPanel )
        return 0;

    //Calculate the origin and angles
    ComputePanelToWorld();

    //Draw the panel
    g_pMatSystemSurface->DrawPanelIn3DSpace( hintPanel->GetVPanel(), m_PanelToWorld, 
        pWidth, pHeight, width, height );

    return 1;
}

The source code is also available in this Gist.