基本 HUD(React)
如何使用 React 添加基本 HUD 以显示角色的生命值和弹药。

创建 React 应用
本教程不会教授如何使用 React。理解本教程需要对该框架有基础了解。你可以在此处找到一些文档和教程。
在你选择的文件夹中,创建一个新的 React 应用,用于构建我们的 UI。我们将使用官方推荐的方式 Vite 来启动一个新的 React 项目。在终端中执行以下命令(需要安装 Node.js):
npm create vite@latest basic-hud -- --template react
cd basic-hud
npm install
你的应用文件夹结构应如下所示:
basic-hud/
├── public/
├── src/
│ ├── assets/
│ ├── App.css
│ ├── App.jsx
│ ├── index.css
│ └── main.jsx
├── index.html
├── package.json
└── vite.config.js
应用的基本配置
在开始添加代码之前,需要配置一些内容以确保你的应用与 nanos world 兼容:
-
在构建中使用相对路径
WebUI 从包文件(file://)中加载你的应用,因此构建产物必须使用相对路径引用其文件。编辑你的 vite.config.js 并添加 base 选项:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
base: './',
})
-
添加 nanos world 事件处理
默认情况下,你的 React 应用无法处理原生 nanos world 事件。要解决此问题,请在应用的 src 目录中新建名为 Events.js 的文件,并添加以下代码:
const Events = {};
Events.Call = function (sEventName, ...args) {
if (typeof (window.Events) == "undefined") return;
window.Events.Call(sEventName, ...args);
}
Events.Subscribe = function (sEventName, callback) {
if (typeof (window.Events) == "undefined") return;
window.Events.Subscribe(sEventName, callback);
}
export default Events;
不要忘记在需要调用/订阅 nanos world 事件的组件中导入此文件
-
清理应用中未使用的文件
应用中默认包含一些示例文件。为了保持项目整洁,我们将移除它们:
./src/assets/react.svg./public/vite.svg
同时清空 ./src/index.css 的内容,因为我们将直接在 App.css 中编写我们自己的样式。
不要忘记移除 ./index.html(网站图标链接)以及 ./src/App.jsx 中对已删除文件的引用。
创建 UI
现在我们的应用已经就绪且与 nanos world 兼容,可以开始编写代码了。由于此 UI 非常简单,我们将直接使用 src 文件夹中默认提供的 App.jsx 组件
import './App.css';
function App() {
return (
<div >
{/* 生命值容器(黑色背景) */}
<div id="health_container">
<img src="./img/health.png" alt="health-cross"/>
<span id="health_current">100</span> {/* 生命值 */}
</div>
{/* 武器弹药容器(黑色背景) */}
<div id="weapon_ammo_container">
<span id="weapon_ammo_clip">30</span> {/* 弹夹数值 */}
<span id="weapon_ammo_bag">/ 1000</span> {/* 弹药袋数值 */}
</div>
</div>
);
}
export default App;
如果你注意到我们使用了一张图片 health.png,可以在此处下载 https://i.imgur.com/0BmQJVZ.png 并将其放置在应用的 ./public/img 文件夹中。
当你引用存储在 public 文件夹中的文件时,无需在路径中添加 public。构建完成后,public 文件夹中的所有内容都会被复制到你应用的根目录下。
现在让我们为其添加样式。请编辑你的 App.css 文件并添加以下 CSS 代码:
body {
font-family: Tahoma, sans-serif;
font-size: 14px;
margin: 0px;
padding: 0px;
margin-bottom: 20px;
}
#weapon_ammo_container {
position: absolute;
right: 0px;
bottom: 0px;
width: 200px;
height: 50px;
background-image: linear-gradient(to right, #00000000, #00000080);
text-align: right;
line-height: 47px;
}
#weapon_ammo_bag {
color: #ededed;
font-weight: bold;
font-size: 16px;
margin-right: 30px;
position: relative;
top: -4px;
}
#weapon_ammo_clip {
color: white;
font-weight: bold;
font-size: 32px;
}
#health_container {
position: absolute;
bottom: 0px;
width: 200px;
height: 50px;
background-image: linear-gradient(to left, #00000000, #00000080);
}
#health_container img {
height: 23px;
margin: 13px;
}
#health_current {
color: white;
font-size: 32px;
font-weight: bold;
position: absolute;
margin-top: 4px;
}
要确保你的应用正常运行,只需启动应用并在网页浏览器中查看最终效果即可。为此,请使用以下命令:
npm run dev
你可以在终端显示的地址(通常为 http://localhost:5173)中查看运行中的应用。
在开发过程中,你可以将 WebUI 指向此地址(WebUI("Main HUD", "http://localhost:5173"))以在游戏内获得热重载支持。发布前请切记切换回构建好的文件!
启动后,你应该会在网页浏览器中看到类似这样的内容:
在应用中添加事件处理
现在应用的基础工作已准备就绪,我们可以开始实现事件处理功能。为此,请编辑你的 App.jsx:
import './App.css';
import Events from './Events.js';
import { useState, useEffect } from 'react';
function App() {
// 使用 React State 存储生命值和弹药数值
// 这些数值由来自服务端的事件进行更新
const [health, setHealth] = useState(100);
const [weaponAmmo, setWeaponAmmo] = useState(30);
const [weaponBag, setWeaponBag] = useState(1000);
const [displayAmmo, setDisplayAmmo] = useState(false);
// 仅在组件挂载时订阅一次事件
// (直接在组件主体中订阅会导致每次渲染时都添加新订阅)
useEffect(() => {
// 订阅与弹药及弹药容器可见性相关的事件
Events.Subscribe("UpdateWeaponAmmo", (enable, clip, bag) => {
setDisplayAmmo(enable);
setWeaponAmmo(clip);
setWeaponBag(bag);
});
// 订阅与生命值相关的事件
Events.Subscribe("UpdateHealth", (health) => {
setHealth(health);
});
}, []);
return (
<div >
{/* 生命值容器(黑色背景) */}
<div id="health_container">
<img src="./img/health.png" alt="health-cross"/>
<span id="health_current">{health}</span> {/* 生命值数值 */}
</div>
{/* 武器弹药容器(黑色背景) */}
{displayAmmo &&
<div id="weapon_ammo_container">
<span id="weapon_ammo_clip">{weaponAmmo}</span> {/* 弹夹弹药数值 */}
<span id="weapon_ammo_bag">/ {weaponBag}</span> {/* 备用弹药数值 */}
</div>
}
</div>
);
}
export default App;
UI 编码部分完成后,你可以对它进行 build,以便将其添加到你的 nanos world 包中!
构建你的 React 应用
完成 Web 部分的开发后,你需要对应用进行构建。在应用的根目录下,运行以下命令:
npm run build
完成后,你将在 ./dist 文件夹中找到你应用的所有文件。
创建 UI 包
在包的 Client 文件夹内,新建一个名为 UI/ 的文件夹(可选),以便将 UI 文件与脚本(lua)文件区分开来:
之后,将 React 应用中 ./dist 文件夹里的所有文件复制到 nanos world 包 的 UI/ 文件夹中。
最后,在包的 Index.lua 中,让我们添加生成和更新 UI 所需的所有代码:
-- 使用你刚刚创建的 HTML 文件生成一个 WebUI
main_hud = WebUI("Main HUD", "file://UI/index.html", WidgetVisibility.VisibleNotHitTestable)
-- 当 LocalPlayer 生成时,在其上设置一个事件,以便在我们控制新角色时触发。此事件仅调用一次,重新加载包时的机制请参考 Package.Subscribe("Load")
Client.Subscribe("SpawnLocalPlayer", function(local_player)
local_player:Subscribe("Possess", function(player, character)
UpdateLocalCharacter(character)
end)
end)
-- 当包加载时,验证 LocalPlayer 是否已经存在(例如在重新加载包时),然后尝试获取并存储其控制的角色
Package.Subscribe("Load", function()
local local_player = Client.GetLocalPlayer()
if (local_player ~= nil) then
UpdateLocalCharacter(local_player:GetControlledCharacter())
end
end)
-- 为本地角色设置所有必要事件的函数(以便在其受到伤害或死亡时更新 UI)
function UpdateLocalCharacter(character)
-- 验证角色是否不为 nil(例如当 GetControlledCharacter() 未返回角色时)
if (character == nil) then
return
end
-- 使用当前角色的生命值更新 UI
UpdateHealth(character:GetHealth())
-- 每当生命值发生变化(受到伤害、受到治疗、死亡或复活)时更新生命值 UI
character:Subscribe("HealthChange", function(charac, old_health, new_health)
UpdateHealth(new_health)
end)
-- 尝试获取角色是否持有任何武器
local current_picked_item = character:GetPicked()
-- 如果持有,则更新 UI(IsA 也匹配继承自武器的类)
if (current_picked_item and current_picked_item:IsA(Weapon)) then
UpdateAmmo(true, current_picked_item:GetAmmoClip(), current_picked_item:GetAmmoBag())
end
-- 在角色上设置事件,以更新其抓取的武器(在 UI 上显示弹药)
character:Subscribe("PickUp", function(charac, object)
if (object:IsA(Weapon)) then
UpdateAmmo(true, object:GetAmmoClip(), object:GetAmmoBag())
end
end)
-- 在角色上设置事件,以在其丢弃武器时移除弹药 UI
character:Subscribe("Drop", function(charac, object)
UpdateAmmo(false)
end)
-- 在角色上设置事件,以在其开火时更新 UI
character:Subscribe("Fire", function(charac, weapon)
UpdateAmmo(true, weapon:GetAmmoClip(), weapon:GetAmmoBag())
end)
-- 在角色上设置事件,以在其重新装填武器时更新 UI
character:Subscribe("Reload", function(charac, weapon, ammo_to_reload)
UpdateAmmo(true, weapon:GetAmmoClip(), weapon:GetAmmoBag())
end)
end
-- 用于更新弹药 UI 的函数
function UpdateAmmo(enable_ui, ammo, ammo_bag)
main_hud:CallEvent("UpdateWeaponAmmo", enable_ui, ammo, ammo_bag)
end
-- 用于更新生命值 UI 的函数
function UpdateHealth(health)
main_hud:CallEvent("UpdateHealth", health)
end
大功告成!你的应用现在应该已经准备好在 nanos world 中使用了。要进行测试,只需运行你的服务器并欣赏你的劳动成果

就这些!你的 React Web UI 现已成功集成到 nanos world 中,随时可以使用。欢迎在你自己的项目中使用此示例 :)