基本概念
本指南适用于想要学习在 nanos world 上开始制作模组和编写脚本的基本概念的初学者。
简介
欢迎来到 nanos world! 这是一款沙盒多人游戏,允许你创建自己的游戏模式、导入自定义资产、地图,并完全使用 Lua 脚本扩展其功能。
在深入研究模组和脚本之前,了解 nanos world 中一切是如何运作的基本概念非常重要。 本页面将为你提供所需了解的一切内容的简要说明,并提供了解每个主题的更多信息的链接!
通读完本指南后,你将准备好开始进行编程、模组制作和内容创作!
资产包
资产包是模组作者将自定义资产(如新模型、纹理、声音等)导入游戏的方式。 它们可以使用虚幻引擎创建、导出,然后作为资产包添加到 nanos world 服务器的 Assets/ 目录下!
每个资产包都有一个名为 Assets.toml 的配置文件。 该配置用于列出包中包含的所有资产、它们的路径以及其他元数据(如它们的类型、类别和标签)。 游戏利用该文件来加载资产,并通过实体使其可在 Lua 脚本中使用。
在 Lua 脚本中,某些实体需要一个资产作为参数。 例如,实体 Prop 在第三个构造函数参数中需要一个 StaticMesh Reference。
通过这种方式,我们使用资产引用来生成这样一个道具:
-- 使用游戏原生内置的默认“nanos-world”资产包
local my_prop = Prop(Vector(), Rotator(), "nanos-world::SM_Cube")
资产包是扩展游戏和添加新功能的强大工具,对于想要为玩家创建独特且令人兴奋的体验的模组作者来说至关重要。 在我们的资产指南中进一步阅读有关资产包的信息:
资产指南core-concepts/assets包
包是 nanos world 的核心构建块,因为它们为脚本编写者提供了一种为游戏创建自定义内容和功能的方法。
包有不同的类型,每种类型都有不同的能力,包括执行 Lua 脚本、创建游戏模式、定义地图,甚至创建自定义的加载屏幕!
脚本是最常见的包类型,它允许脚本编写者在客户端和服务器上运行 Lua 代码,从而可以监听事件、与游戏类交互并以各种方式修改游戏行为。
在服务器运行期间,可以使用 package reload all 动态加载/卸载包!
每个包都是服务器 Packages/ 目录下的一个文件夹,内部包含一个配置文件 Package.toml,我们在其中定义包的类型及其相关设置。
包还可以依赖于其他的包和资产包,这允许脚本编写者跨多个包复用代码和资产。 这有助于减少重复并改善组织结构,尤其是对于较大的脚本或游戏模式。
如果我们想要加载一个包,必须在服务器的 Config.toml 中定义它们。我们为每个包类型(game_mode、loading_screen、map 和 packages)都设置了一个部分,我们必须在其中列出希望服务器加载的包名称!
总的来说,包是 nanos world 模组制作中的一个基本概念,它们提供了一种强大且灵活的方式来为游戏创建自定义内容和功能。 在我们的包指南中进一步阅读:
包指南core-concepts/packages/packages-guide脚本
脚本是编程和自定义 nanos world 的强大方式。 它允许我们监听游戏事件,随意生成实体、与之交互并改变它们。
所有 Lua 代码都通过包来执行,可以拥有仅在服务器上、仅在客户端上或在两者上运行的脚本。
学习
如果你是编程新手,YouTube、哔哩哔哩或任何课程网站都是开始学习各种编程语言编码基础知识的好地方。
对于 Lua 脚本,官方的 Lua 手册提供了关于该语言基础知识的详尽教程。
此外,我们的文档是学习 nanos world 中可用一切内容的极佳资源。 你可以浏览我们的 游戏模式和包 类别来查找真实的示例!
🎓 核心概念部分提供了指南和极好的信息,而👨💻 脚本参考则提供了关于我们 API 使用的技术细节!
此外,我们的官方 Discord 是一个活跃的社区,你可以在那里提出问题、获得帮助并与其他脚本编写者建立联系!
包文件夹结构
一个脚本包的文件夹结构包括 Server/、Client/ 和 Shared/ 文件夹。 你可以在每个文件夹中放置 .lua 文件,以便在服务器、客户端或两者(共用)上执行代码:
my-package/
├── Server/
│ ├── Index.lua
│ └── *.lua
├── Client/
│ ├── Index.lua
│ └── *.lua
├── Shared/
│ ├── Index.lua
│ └── *.lua
└── Package.toml
只有 script、game-mode 和 map 类型的包才会具有这种结构,因为只有这些类型才会运行 Lua 脚本。
包仅加载每个基础文件夹(Server/、Client/ 或 Shared/)的 Index.lua 文件,该文件负责导入其他脚本文件。
你可以在左侧边栏的**🎓 核心概念 / 脚本** 部分下找到若干脚本编写指南。
虚幻引擎概念
如果你不熟悉虚幻引擎,某些概念可能会听起来有些令人困惑。 由于 nanos world 是构建在其之上的,因此我们大多沿用了虚幻引擎自身的概念和术语。
向量、旋转量与单位
在虚幻引擎中,世界是使用坐标系在 3D 空间中表示的。 物体在此空间中的位置使用 Vector(向量)来表示,它本质上是一组由三个数字组成的集合,分别代表 X(前)、Y(右)和 Z(上)坐标。
物体的朝向使用 Rotator(旋转量)来表示,它也是一组由三个数字组成的集合,分别代表 Pitch(绕 Y 轴旋转)、Yaw(绕 Z 轴旋转)和 Roll(绕 X 轴旋转)。
虚幻引擎默认使用厘米作为度量单位,因此当你看到一个 Vector 的值为 (100, 0, 0) 时,这意味着该物体距离原点向前 100 厘米(即 1 米)。
Actor
在虚幻引擎中,Actor 是指任何可以被放置在世界中的物体。 它是所有在游戏世界中具有物理外观的物体的基类,例如 Character(角色)、Prop(道具)、Weapon(武器)等。 它们通常可以在生成时指定 location(位置)和 rotation(旋转)。
玩家 vs 角色 vs Pawn
理解 Player、Character 和 Base Pawn 之间的区别是一个非常重要的概念。
在虚幻引擎中,PlayerController 是代表服务器中已连接玩家的实体,而 Pawn 是玩家在世界中的物理代表,并且是可以被控制的。 Character 则是一种特殊的 Pawn,它拥有诸如行走、跳跃等额外功能。
对于 nanos world,我们对这个概念进行了一些简化:我们只有一个代表玩家的 Player 类。 以及几个代表玩家在世界中物理形象的角色类,例如 Character 和 CharacterSimple。
因此,当一个 Player 生成时,意味着一个真实的人刚刚连接到了服务器。
日志与调试
你可以使用 Console 静态类将消息输出到控制台,该类提供了不同的日志级别,例如 .Log()、.Debug()、.Warn() 和 .Error():
-- 标准控制台输出
Console.Log("普通脚本日志!")
Console.Debug("仅当日志级别 ≥ debug 时才会出现的调试消息!")
Console.Warn("警告橙色消息!将显示堆栈跟踪!")
Console.Error("错误红色消息!将显示堆栈跟踪!")
类
类是可以根据你的意愿生成、交互和更改的实体。 一个生成 Light 和 StaticMesh 的示例如下:
local my_light = Light(Vector(0, 100, 100), Rotator(), Color.RED)
local my_static_mesh = StaticMesh(Vector(0, 0, 100), Rotator(), "nanos-world::SM_Cube_VR_01")
你甚至可以按照我们想要的方式与现有实体进行交互和更改,例如对一个现有的 Prop 进行操作:
local some_prop = Prop.GetByIndex(1)
some_prop:SetMesh("nanos-world::SM_Pyramid_VR")
some_prop:SetLocation(Vector(100, 100, 100))
local prop_rotation = some_prop:GetRotation()
在类指南中进一步阅读有关类的信息:
类指南core-concepts/scripting/classes-guide静态类
除了类之外,我们还拥有静态类,它们只是包含函数的 Lua 库,可以直接调用而不需要生成实体或拥有实例。
例如,我们可以使用 Chat 静态类发送一条聊天消息:
Chat.BroadcastMessage("你好世界!")
或者在客户端使用 Trace 静态类进行射线投射:
local start_location = Vector(0, 0, 100)
local end_location = Vector(1000, 0, 100)
local collision_channels = CollisionChannel.WorldStatic
local trace_mode = TraceMode.DrawDebug | TraceMode.TraceComplex
-- 进行跟踪
local trace_result = Trace.LineSingle(start_location, end_location, collision_channels, trace_mode)
Events
在 nanos world 中,API 中的一切都是基于事件的,这与其他游戏中的钩子类似。
例如,当 Player 生成(Spawn)、Character 进入载具(EnterVehicle)或受到伤害(TakeDamage)时,都会触发对应的事件。
你可以使用 Lua 脚本监听这些事件并执行相应的操作。 例如,以下是如何得知玩家何时加入服务器或道具何时被生成的代码:
-- 当玩家加入服务器时订阅
Player.Subscribe("Spawn", function(player)
Console.Log("玩家 %s 加入了服务器!", player:GetName())
end)
-- 当道具生成时订阅
Prop.Subscribe("Spawn", function(prop)
Console.Log("道具 id %d 刚刚生成了!", prop:GetID())
end)
在事件指南中进一步阅读有关事件的信息:
事件指南core-concepts/scripting/events-guide计时器
通过我们的 Timer 静态类,你可以创建计时器和延迟,以便在过去特定时间后执行代码。 这对于创建定时事件、冷却时间或任何你想要延迟执行某些代码的情况都非常有用。
我们的计时器 API 类似于 JavaScript 计时器,你可以使用 Timer.SetTimeout() 在延迟后执行一次函数,或者使用 Timer.SetInterval() 在指定的间隔内连续重复执行某个函数。
-- 每 10 秒运行一次函数
Timer.SetInterval(function()
Console.Log("每 10 秒触发一次!")
end, 10000)
-- 1 分钟后运行一次函数(仅执行一次)
Timer.SetTimeout(function()
-- 执行某些操作
end, 60000)
在计时器页面中进一步阅读有关计时器的信息:
计时器静态类scripting-reference/static-classes/timer查找现有实体
除了在生成实体时存储一个变量引用,或者通过事件获取其引用之外,有时你还需要获取一个已经生成的实体。
例如,我们可以使用类的静态方法 .GetAll() 或 .GetPairs() 来获取该类所有已生成的实体:
-- 返回所有已生成角色的表副本
local all_characters = Character.GetAll()
-- 高效地遍历所有已生成的玩家
for _, player in pairs(Player.GetPairs()) do
Console.Log("玩家 %s 已连接!", player:GetName())
end
或者,我们可以使用 .GetByIndex() 方法通过特定的索引来获取对现有 Prop 的引用:
local my_prop = Prop.GetByIndex(10)
另一种方法是通过全局唯一 ID,使用 Server.GetEntityByID() 或 Client.GetEntityByID() 方法来检索实体:
local my_character = Character(Vector(), Rotator(), "nanos-world::SK_Male")
local character_id = my_character:GetID()
-- 随后在其他地方...
local my_character = Server.GetEntityByID(character_id)
获取玩家角色
正如在 玩家 vs 角色 vs Pawn 小节中所解释的,一个 Player 代表服务器中连接的玩家,而 一个 Character 是该玩家在世界中的物理代表。
你可以使用 Player:GetControlledCharacter() 方法来获取玩家当前控制的角色。
在客户端获取本地玩家
在客户端,有一些特殊的函数和事件可以用来获取本地玩家:Client.GetLocalPlayer() 和 SpawnLocalPlayer (Client):
local local_player = Client.GetLocalPlayer()
-- 注:本地玩家只有在客户端的 "SpawnLocalPlayer" 事件被触发后才可用
if (local_player) then
local local_character = local_player:GetControlledCharacter()
-- 获取我角色的位置
Console.Log("我的角色位于 %s", local_character:GetLocation())
end
Client.Subscribe("SpawnLocalPlayer", function(player)
Console.Log("我的玩家刚刚生成在客户端!%s", player:GetName())
-- 只有在此处之后,才能够访问 Client.GetLocalPlayer()
end)
网络
我们的客户端-服务器架构在设计时就考虑到了与网络和同步相关的一切内容的轻松和简单性。 让脚本编写者只需专注于创意部分!
在服务器上生成的所有实体也会自动在客户端上生成并同步。
当在服务器端调用实体的某个函数时,该函数也会自动在每个连接的客户端上的相同实体上调用。 使每个人始终与所有更改保持同步!
例如,如果我们生成一个 Character 并设置其位置,所有客户端的位置也将同步更新:
-- 生成一个角色(在服务器端)
local my_character = Character(Vector(0, 0, 0), Rotator(), "nanos-world::SK_Male")
-- 设置其位置
my_character:SetLocation(Vector(100, 100, 100))
某些类只能在服务器上生成,有些只能在客户端上生成。 它们的某些函数也是如此。 通过这种方式,如果你在客户端生成一个实体,它将仅对该客户端存在。
通过本文档,你会发现权威工具提示,告知你一个函数是可以在服务器上调用 、在客户端上调用
、在两者上调用
还是仅在生成它的那一端调用
!
两端之间的通信
为了能够将信息从服务器发送到客户端或反之亦然,我们在 Events 静态类中开放了一些特殊方法来实现这一点!
-- 在客户端订阅一个远程事件
Events.SubscribeRemote("MyClientEvent", function(my_text)
Console.Log("服务器收到事件!" .. my_text)
-- 输出 "服务器收到事件!你好 nanos world!"
end)
-- 向所有客户端包中的所有玩家发送一个远程事件
Events.BroadcastRemote("MyClientEvent", Reliability.Reliable, "你好 nanos world!")
在我们的“在包之间进行通信”指南中进一步阅读有关网络事件的信息:
在包之间进行通信core-concepts/scripting/communicating-between-packages同步值
除了通过作为动态事物的事件发送数据之外,我们还可以将自定义值绑定/设置到实体上,甚至全局设置在服务器/客户端中。
例如,我们可以在任何实体上设置一个值,该值可以被任何包访问:
-- 设置道具的“my_value”值
my_prop:SetValue("my_value", 100)
-- 稍后再次获取该值
local my_value = my_prop:GetValue("my_value")
这将使 my_value 在该端(客户端或服务器)上全局设置在此实体上。
但我们也有能力在实体上发送一个同步值(如果我们处于服务器端)! 为此,我们只需附加 true 参数,告知其也要发送并同步到所有客户端!
-- 将同步的“my_value”值设为道具
my_prop:SetValue("my_value", 100, true)
这些值可由任何包访问,是同步自定义值的绝佳方式!
在实体值指南中进一步阅读有关实体值的信息:
实体值core-concepts/scripting/entity-valuesUI
用户界面是游戏的一个至关重要的方面,我们提供了几种灵活的方式来创建 UI!
WebUI
最直接的方式是使用 WebUI,它允许你使用 HTML/CSS/JavaScript 创建和集成 UI,拥有绝对的自由度来根据需要进行创建和自定义。
可以使用事件在 Lua 和 JavaScript 之间进行通信!
有关更多信息,请参阅我们的 WebUI 教程:
基本 HUD(HTML)getting-started/tutorials-and-examples/basic-hud-htmlWidget
另一种全新且更高级的 UI 创建方式是使用虚幻 Widget! 借助它们,你可以完全访问强大的控件,甚至可以通过虚幻引擎创建自己的控件并将其导入游戏,从而提供无缝的用户体验!
有关更多信息,请参阅我们的控件类页面:
控件类scripting-reference/classes/widget地图
地图(在虚幻中称为关卡)是游戏的入口,这是加载世界并在客户端生成实体的地方。
为了配置地图,我们创建了一个 map 类型的包。这种类型类似于 script,但其 Package.toml 具有额外的设置来配置地图,例如我们有 map_asset = "my-asset-pack::MyLevel",我们需要用它来定义该包将加载哪个地图资产。
我们可以在 Config.toml 的 map = "my-map-package" 设置下配置服务器将运行哪个地图。
我们提供了 4 个内置地图,无需任何额外的包或资产包即可加载:default-blank-map、default-empty-map、default-ocean-map 和 default-testing-map。
此外,在地图的 Package.toml 中,我们可以配置 spawn_points 和 custom_data,任何包都可以通过脚本访问它们:
local spawn_points = Server.GetMapSpawnPoints()
local custom_data = Server.GetMapConfig()
正如你所注意到的,通过加载地图,你也在加载一个带有脚本的包,因此你可以让自定义脚本针对每个地图运行。 这对于添加诸如可交互环境或门之类的自定义行为非常有用。
游戏模式
游戏模式是一种包类型,其行为与 scripts 完全相同,唯一的区别是我们在同一时间只能加载一个游戏模式,而我们可以运行任意数量的脚本。
它们应该被用来创建独特且自成一体的游戏体验。
玩家可以通过在主菜单的“新游戏”部分选择一种游戏模式来轻松选择并启动服务器。
结语
在本指南中,我们介绍了 nanos world 脚本编写和模组制作的一些基本概念。 理解这些概念非常重要,因为它们构成了构建任何游戏模式或脚本的基础。 我们希望你现在可以开始创作内容并将你的想法变为现实!
如果你有任何疑问,请随时在我们的 Discord 上提出! 我们的社区非常棒且非常友好,我们将乐意为你提供帮助!
现在我们建议从快速开始指南开始,创建你的第一个包!
快速开始getting-started/quick-start