HUD 元素
It is not recommended to use machine translation without any corrections.
If the article is not corrected in the long term, it will be removed.
Also, please make sure the article complies with the alternate languages guide.
2024年1月
在游戏中,通过使用 HUD(抬头显示)向玩家传递信息通常十分重要。HUD 通常包含玩家的生命值、携带的弹药数量,或关于目标的消息。许多不同的元素共同构成了这个 HUD。本文将介绍如何创建这些元素,并利用它们向用户显示信息。
HUD 元素通常使用 VGUI2 库(除了
反恐精英:全球攻势 以及一些第三方游戏,例如
黑山)来渲染其状态。这使得它们不仅在外观和操作上与游戏中的其他 VGUI 元素保持一致,还能使用经过脚本处理的、可动的动画组件,从而大大提升视觉质量。这就要求它们既要有在客户端声明的代码组件,也要有驻留在客户端且独立于代码之外的脚本文件。
HUD 元素类应继承自 CHudElement 基类。此基类负责更新、绘制,以及根据特定的游戏状态隐藏 HUD 元素(例如,可以设置 HUD 元素在玩家死亡时消失等)。该类还会解析游戏 /scripts 目录下的 HudLayout.res 文件,以确定所有元素的位置和行为。大多数元素还将继承自 vgui::Panel 基类。这为它们提供了一个基本的画布,可以在上面绘制文本、形状或纹理。有关 vgui::Panel 类的更多信息,请参阅此 SDK 中提供的 VGUI2 文档。
HUD 消息
元素使用一个简单的系统来捕获从服务器发送的消息。一个 HUD 元素会声明一个回调函数以及与其关联的对应消息。当收到消息时,回调函数通常会从消息中读取一些信息,并显示或以其他方式更新其视觉状态。一个例子是,当玩家受到伤害时,服务器会发送的 Damage 消息。该消息中编码了伤害值以及受伤部位。生命值 HUD 元素接收到此消息后,会播放动画并更改其数字读数,以反映玩家的新生命值。
消息处理器由 HUD 元素通过以下宏声明:
DECLARE_HUD_MESSAGE( CMyHUDClass, MyHUDMessage );
第一个参数指定声明消息处理器的类,第二个参数是相关的消息。此宏会将消息的名称展开为一个回调函数,该函数必须在 HUD 元素类中定义。在本例中,创建的回调函数将是:
void MsgFunc_MyHUDMessage( bf_read &msg );
必须使用上述声明来编写此函数的原型和主体。bf_read 类是一个带有多种查询方法的数据缓冲区,用于在服务器和客户端之间传输未格式化的数据。服务器使用 bf_write 类来发送这些数据。
HUD 元素还必须包含 HOOK_HUD_MESSAGE 宏定义,通常放在 HUD 元素的 Init() 函数中。基于我们上面的示例,其定义将是:
HOOK_HUD_MESSAGE( CMyHUDClass, MyHUDMessage );
此宏注册消息并将其链接到我们之前定义的回调函数。如果未能包含此声明,当服务器发送用户消息时,将导致断言和错误。
从服务器发送消息
为了使客户端能正确接收消息,必须在服务器上声明并发送这些消息。这是通过用户消息单例中的 Register() 函数来实现的。
void CUserMessages::Register( const char *name, int size )
|
| 此函数创建一条消息定义,并在会话期间一直保留。发送消息时,此处指定的名称将用于在向客户端发送时识别该消息。
name
size
|
所有用户消息都应在 RegisterUserMessages() 全局函数中声明。此函数在用户消息单例实例化时被调用。未声明的用户消息将无法被客户端正确接收,并且尝试这样做会产生错误。
如果遵循了上述所有步骤,HUD 元素现在应该拥有一个用于在服务器和客户端之间发送和接收消息的可运行基础设施。要发送一条消息,我们使用 UserMessageBegin()、MessageEnd() 以及下面列出的支持宏。
UserMessageBegin() 函数的定义如下:
void UserMessageBegin( IRecipientFilter& filter, const char *messagename )
|
此函数构造指定类型(按名称)的用户消息,并准备接收用户的数据。过滤器可以是 ../dlls/recipientfilter.h 中定义的任何 IRecipientFilter 类型(例如 CSingleUserRecipientFilter、CBroadcastRecipientFilter 等)。
filter
messagename
|
以下宏提供了向发送到客户端的数据流写入数据的功能。它们必须按照发送的顺序被接收和处理。这些宏的写法如下:
...
WRITE_BYTE( m_uchMyByte );
WRITE_VEC3COORD( m_vecMyOrigin );
WRITE_BOOL( m_bMyState );
...
以下是所有可用于向消息流写入数据的宏的描述:
WRITE_BYTE |
一个字节 |
WRITE_CHAR |
一个字符 |
WRITE_SHORT |
一个短整型 |
WRITE_WORD |
一个字 |
WRITE_LONG |
一个长整型 |
WRITE_FLOAT |
一个浮点数 |
WRITE_ANGLE |
无符号 8 位角度值 |
WRITE_COORD |
压缩坐标值 |
WRITE_VEC3COORD |
来自 Vector 类型的压缩坐标值 |
WRITE_VEC3NORMAL |
来自 Vector 类型的压缩法线值 |
WRITE_ANGLES |
来自 Vector 类型的压缩角度值 |
WRITE_STRING |
字符串 |
WRITE_ENTITY |
实体索引(短整型) |
WRITE_BOOL |
一位布尔值 |
WRITE_UBITLONG |
无符号位长整型值 |
WRITE_SBITLONG |
有符号位长整型值 |
WRITE_BITS |
通过参数指定位数的位值 |
在使用 WRITE_ 宏之后,必须通过 MessageEnd() 函数终止并发送消息。
在客户端接收消息
一旦服务器发送了消息,客户端将通过钩挂到该消息的回调函数接收它。接收回调函数会获得一个 bf_read 类实例,其中包含从服务器传递过来的数据。该类包含从数据流中读取格式化数据的工具函数。同样,必须按照发送的顺序读取数据。
现在,数据已经在服务器和客户端之间发送和接收,可以使用绘制函数(如 VGUI2 文档中所述)基于这些数据绘制所需的任何信息。
要在客户端捕获这些消息,必须钩挂它们。添加消息钩子需要确保其类实例不会被销毁。
class CHudThingy : public CHudElement , public vgui::Panel
{
DECLARE_CLASS_SIMPLE( CHudThingy, vgui::Panel ); // 这很重要
....
void MsgFunc_SayText(bf_read &msg) { /* 对消息进行处理 */ };
}
DECLARE_HUD_MESSAGE( CHudThingy, SayText );
// 初始化高级聊天框
void CHudAdvancedChat::Init( void )
{
...
HOOK_HUD_MESSAGE( CHudThingy, SayText );
}
示例代码
在以下文件中可以找到一个 HUD 元素示例,这些文件包含在示例应用程序中:
../cl_dll/sdk/sdk_hud_message.cpp
../dlls/sdk/sdk_env_message.cpp
../game_shared/sdk/sdk_usermessages.cpp
在游戏中显示用户消息(代码 - 服务器插件)
在这种情况下,可以将此函数添加到项目中,该函数包含完整注释。
void YourPlugin::SayTextMsg(int PlayerIndexN, const char *Message)
{
MRecipientFilter filter;
if(PlayerIndexN == 0)
{
filter.AddAllPlayers(MaxClients); // 我们在 ServerActivate 函数中获取最大客户端数
}
else
{
filter.AddRecipient(PlayerIndexN); // 添加该玩家
}
bf_write *pWrite=engine->UserMessageBegin(&filter, 3); // 3 代表 Say_Text
if( !pWrite )
{
//TODO: 发生错误时要执行的操作
}
else
{
pWrite->WriteByte(PlayerIndexN); // 玩家索引,要从服务器发送全局消息,请将其设为 0
pWrite->WriteString(Message); //消息本身
pWrite->WriteByte(0); //0 为根据颜色进行短语处理,1 为忽略
engine->MessageEnd(); //结束发送
}
}
用法:
void YourPlugin::ClientCommand(edict_t *pEntity)
{
const char *pcmd = m_Engine->Cmd_Argv(0);
if ( !pEntity || pEntity->IsFree() )
return; // 仅当实体存在时才继续
if ( FStrEq( pcmd, "ClientMsg" ) )
SayTextMsg( engine->IndexOfEdict(pEntity), "Hello World!"); // 只有输入此命令的玩家能看到这条消息!
else if ( FStrEq( pcmd, "SayServer" ) )
SayTextMsg( 0, "Hello World!"); // 游戏中所有人都能看到这条消息:)
...
RecipientFilter 需要修改,请参阅 此处获取更多信息。