
关于《拆迁》UI/UX设计的知识总结,以简单易懂的形式呈现。涵盖理论与实践。 在这里,我们将一起从零开始为一个模组构建玻璃态选项菜单。 00 介绍

大家好!我是Novena,大家可能更熟悉我的模组《Volatile Blackholes》。在本指南中,我将分享我关于《拆迁》模组UI/UX设计的大部分知识。我们将涵盖理论部分(构思创意、相关设计法则和规则以及外部软件)和实践部分(创建资源并在游戏中组装UI)。 本指南将分为不同的章节,每个章节涵盖UI设计的一个不同方面。 如果本指南反响良好,我将继续推出一系列关于不同主题的模组制作指南。 - 理论、法则、规则及其他概念 01 用户界面的重要性 用户界面(UI)是用户与计算机或应用程序进行交互的空间。它包含所有视觉和听觉元素,这些元素使用户能够进行交流并接收反馈。换句话说,用户界面是你在屏幕上看到的、允许你与模组交互或提供某些信息的部分。因此,即使是简单的文本也被视为用户界面。 根据这个定义,我们可以确定用户界面的目标是以有条理且用户友好的方式呈现信息。 以下是两种不同方式显示同一组信息的示例。

注意在第一个设计中,重要元素更难区分开来,即使是细微的改动也能产生巨大影响 每个用户界面的设计都有其特定目标。例如,商店网站应确保购物车易于访问,商品及其价格信息清晰传达,结账流程应尽可能便捷,最好能一键完成!优秀的用户界面能促进更多商品销售,从而带来更多收益! 这一点同样适用于你的模组。当玩家进入模组的选项菜单时,他们通常有明确的目标,比如降低音乐音量、更改控制方案或探索新设置。作为设计者,突出每个选项的标题并简化其开关操作非常重要。设计糟糕的选项菜单会让玩家感到沮丧,并在你的模组页面留下许多愤怒的评论! 总而言之,用户界面(UI)与模组的其他任何方面同样重要。设计良好的UI能提升模组的易用性,使其操作更快捷、高效且易于导航。它应帮助玩家轻松找到并获取所需信息,例如选项或控制方案。相反,设计糟糕的界面可能会导致玩家不满,在最糟糕的情况下,甚至会让玩家完全放弃使用你的模组。 02 保持简洁 “好的设计是尽可能少的设计”——迪特·拉姆斯 这意味着要聚焦于核心功能,并为用户保持尽可能简洁的体验。同时,这也意味着屏幕上的颜色、文字和杂乱元素要更少。为你的设计选择几种互补色并保持一致使用。要建立层次感,可运用调色板中的不同色调。记住,颜色较深或对比度较低的元素会显得不太重要,字体大小也是如此。

像Coolors这样的网站可以帮助你为用户界面找到好看的配色方案。段落宽度不应超过75个字符,长度应在75到100个单词之间。段落越长效果越差,因为用户阅读的动力会降低。如果你必须显示大段文本,尽量将其拆分成更小的部分,或者使用字体粗细来强调重要内容。

第一段感觉太长,读起来费劲,不利于快速浏览。 第二段较短,更容易快速浏览。 尽量只向用户展示最核心的部分和信息。如果必须展示某些数据,要进行简化并以图表形式呈现,或者将额外数据隐藏在“显示更多”按钮下或单独的高级页面中。 --- 格式塔理论告诉我们,整体大于部分之和。 换句话说,我们的大脑会先分析事物的整体形状和形式,之后才开始注意细节。

你的大脑首先会将其视为一个右向箭头,而非由单个发光点组成的网格。 同样地,在设计布局时,重要的是先确定整体的位置和形状,然后再将其细化为更详细的元素。 例如,在创建选项菜单时,首先通过间距来定义选项的主要类别。接着,明确每个选项,最后添加标题描述和切换开关。

用户首先会识别出3个主要的选项区块,然后开始阅读其中一个区块并识别出3个条目,之后他们会阅读该选项的标题和描述。 记住,始终保持简单。这对用户来说更容易,对你来说工作量也更少。这是双赢的做法。 03 结构 万物皆有其结构和秩序。你的电脑使用文件夹结构来组织所有内容。你有一个主根文件夹,里面是子文件夹,每个子文件夹负责存放其他类型的文件,而这些文件又包含特定的信息。 同样,用户界面也是通过容器来构建结构的。 顾名思义,容器就是用来容纳某些东西的,在我们的例子中,这些东西就是用户界面元素,例如按钮、滑块、文本、图像等。在《拆迁》中,我们可以使用UiWindow()来定义这种容器,其正式名称为渲染上下文。渲染上下文定义了屏幕上的一块空间区域,我们可以在其中定位元素。这也将成为通过组合多个较小的UiWindow()来创建自定义元素的基础。
嵌套UiWindow()定位元素的实用示例。所有内容都位于主红色容器内,然后我创建了两个独立的容器,一个在左侧,一个在右侧。左侧容器同时包含时钟和天气预报。 在渲染上下文中定位元素还能让我们轻松定位整个元素组,例如按钮列表。这样,如果我们需要在界面中为新元素腾出更多空间,只需将整个容器移到其他位置,而不必单独重新定位每个元素。 我们将要使用的另一个重要概念是响应式设计。 响应式设计指的是界面及其中的元素会自动调整以适应不同的屏幕尺寸,并且能够实时进行调整。这也是像YouTube这样的网站能够在大屏幕电视和小屏幕手机上都正常运行的原因,无需为每种可能的屏幕尺寸单独制作和设计界面版本!我们可以使用当前屏幕宽度和高度的百分比来定义元素的大小,而不是固定的像素尺寸。这使得我们的界面无论屏幕大小如何,都能始终适配!注意:如果有人使用超宽显示器,用户界面也会拉伸以填满可用空间。解决此问题的方法是使用限制数学函数来限制其大小范围。有时,用绝对像素定义大小仍然是合适的,例如图标或图像。要获取当前渲染上下文的高度和宽度,我们可以使用UiWidth()和UiHeight()函数。 如果我们想让某个元素的宽度为显示器宽度的80%,只需写成“UiWidth() * 0.8”。例如,普通16:9显示器的宽度为1080像素,那么80%就是864像素;如果有人在4:3显示器(宽度720像素)上玩游戏,该元素的宽度就会是576像素。 由此可见,如果我们直接将宽度硬编码为864像素,那么使用小尺寸显示器的玩家最终会看到一个被截断且无法正常使用的界面! 本节要讨论的最后一点,也可能是最重要的一点,就是【抽象】。 抽象是将复杂事物简化为核心要素的过程。 举个例子,想想汽车是如何制造的。它由哪些部件组成?你最初的想法可能是“一个引擎、四个轮子、一个车身和一个车架”之类的。你刚才所做的就是抽象,你没有去思考组成引擎的每一个螺栓和螺母,而是忽略细节,专注于重要的部分。 这正是为什么给设计赋予恰当结构如此重要。正确嵌套渲染上下文能让我们将思考简化为想要展示的主要元素,而不是去考虑每一个单独的UiRect()和UiText()。 04 通过模式和相似性进行沟通 正如我们已经了解的,用户界面是玩家与模组进行交互的媒介。要与他人或事物进行交流,你需要一种语言。 语言就是一套具有不同含义的规则、模式和符号。在界面设计中,我们可以利用颜色、图标、声音和通用模式等属性来表示特定的状态或操作。 下面我们来逐一了解这些属性: 颜色 构建美观且有意义的调色板的最简单方法,是使用中性的背景色和文本色(或者说是色调),再搭配一种主色。 这三种颜色本身不具有任何特定含义,因为它们是我们最常使用的颜色。 确定主色调方案后,下一步就是选择语义化颜色。 这些颜色的作用是表示特定的状态或操作。例如,强烈的红色用于标记负面或危险操作,如删除文件或关闭选项。 你可以根据需要使用多种此类颜色,但一般规则如下: 1 积极色(例如绿色,用于启用选项、保存进度) 1 消极色(例如红色,用于禁用选项、删除文件) 1 警告色(例如黄色或橙色,用于吸引注意) 1 操作色(例如蓝色,用于标记交互元素)
颜色调色板示例 这些只是示例,因为颜色是主观的,没有预定义的含义。红色可以表示爱与浪漫,但也可能是暴力和危险的信号。黄色可以是充满活力和有趣的,但也可能引人警惕。 重要的是要保持一致性 颜色的另一个作用,或者更确切地说,颜色的深浅,是展示层次结构和深度。 元素颜色越深,看起来就越有深度。大多数情况下,像按钮这样的交互元素会使用最浅的色调。如果你希望用户点击某个重要按钮,应该用你的行动色来标记它。

只需改变按钮颜色,我们就能在潜意识中引导用户点击按钮。 为背景色寻找合适色调的最佳方法是:以主色为基础,然后为每个所需的深度层级将明度值提高5%(假设使用HSL色彩模式)。对于悬停状态等元素,直接选用色调中最浅的颜色即可。 记住,元素层级越高,其背景颜色应越浅! 图标 图标的目的是打破设计中的语言和文化障碍。并非所有人都懂英语,也不是所有人都知道绿色代表积极意义!因此,为了增强沟通效果,我们使用小巧、简洁的图像,即图标。 图标更容易设计得当,因为它们本质上就是象形符号。最佳方法是直接借鉴他人,并采用已被认可的操作含义。需要返回主页按钮?那就使用房子图标。需要取消按钮?那就使用X图标。
YouTube网站在其左侧菜单使用图标,这提高了浏览便捷性,也让用户更容易找到所需选项。

使用Inkscape制作自定义图标集的示例 你可以在Google Fonts上获取许多精美的图标。如果这些图标都不符合你想要的风格,你仍然可以将它们作为参考。记住不要过度使用图标,有时不使用图标反而更好。我们不希望设计变得杂乱! 声音 声音设计可能是最困难的部分,因为它具有主观性,没有绝对的对错之分。你只需要找到感觉。尝试不同的预制声音,找出适合你风格的,然后在此基础上尝试自己设计。良好的声音设计可以将普通的用户界面提升为超级令人满意的体验。添加短促的触感声音,例如点击声,可以增加额外的反馈层次。界面应具备的基本音效包括鼠标悬停音效和点击/交互音效。《拆迁》内置了一些预设的用户界面音效,你可以直接使用这些音效,无需自行设计。(你可以在《拆迁》的data/snd文件夹中找到这些音效) 通用设计原则: 不要让用户每次打开界面新页面时都必须重新熟悉布局。 一旦设计了某个元素,应尽可能多地重复使用。这意味着如果你创建了一个自定义按钮,几乎要在所有地方都使用它。不要过度设计20种不同的按钮。只有当执行的操作差异大到需要改变时,才应使用不同的设计。例如,用滑块调节音量,但用输入框输入电话号码(想象一下,如果你的银行让你用滑块输入电话号码会怎样)。 还要尽量保持不同元素的视觉外观相似,甚至可以将一个元素作为另一个元素的基础。 确保所有内容在不同页面的位置一致很重要。不要在不同提示间改变取消按钮的位置,始终将其放在同一侧。导航也是如此,最糟糕的做法就是每次都把按钮放在不同的地方。不要让用户去费力寻找信息,那样会让人感到沮丧。保持一致性是关键。 构建一套语言体系和规则,并严格遵守。只有在必要时,或者你确定打破规则能提升体验的情况下,才可以这么做。 05 寻找灵感(和动力) “创造力是一个过程,而非某个瞬间” 不要害怕借鉴他人;首先,这是你学习和获取灵感的方式。 找到你喜欢的事物并尝试重现它;或许在制作过程中你会产生好点子,并为其增添有趣的创意。
在为Lyra推进器背包的跨菜单进行设计时,我从游戏【死亡搁浅】中获得了灵感,决定制作类似的内容。 像Dribbble和Game UI Database这类网站会成为你的得力助手。你也可以查阅热门网站和设计师资料来寻找灵感。 记住,每一个设计最初都并不完美。你只需相信这个过程。 保持谦逊,按自己的节奏努力,并根据需要不断重复。有时你可能需要重做某个设计20次,才能在第21次时得到满意的结果。

在设计音乐播放器时,我曾感到毫无头绪。我尝试了多种设计方案,最终放弃了将波形图置于按钮后方的想法,重新调整了布局。 06 外部软件 要制作出色的用户界面,你需要一些外部工具来创建素材,例如音效、图标和其他图形。以下是我之前使用过的一些推荐工具: 音频类: Audacity - 我正在使用的音频编辑工具,专门用于音频编辑,是音频设计的必备工具。 LMMS - 我偶尔还会使用,但已转向Cakewalk。对于初学者,我推荐这款工具,因为它操作更简单。 Cakewalk by Bandlabs - 一款出色的数字音频工作站(DAW)。LMMS是其简化替代版本,但如果你有音频处理经验,建议选择Cakewalk。 Vital Synth - 这是一个插件,因此你需要上述两款数字音频工作站中的任意一款才能运行它。我用它来制作用户界面音效。 图像方面: 位图: GIMP 3 - 我正在使用这款软件 Photoshop(付费) Krita - 我以前用过,但不太喜欢,因为它更适合绘画而非图像编辑 矢量图: Inkscape - 我正在使用这款软件。你可以用它来制作图标。 SVGator(网站) - 我用它来制作动画 Graphite - 我还没使用过,但有所耳闻。它有点像2D版的Blender Blender - 这是一款3D软件,但其着色器节点对于制作纹理非常有用。你可以使用基础和高级数学知识创建各种渐变和图案。值得一试。 在本指南中,我只会讲解如何使用:Audacity、LMMS、Vital、Inkscape和GIMP 3。 这不会是完整的教程,只包含必要的内容。- 练习:共同创建用户界面 07 与光标建立友好关系(《拆迁》用户界面代码背后的技术细节) 了解光标 《拆迁》渲染用户界面的方式是从上到下读取代码,然后在光标位置单独绘制每个元素。 光标就像一只虚拟的海龟,可以在我们当前的渲染环境中移动,在屏幕上绘制各种元素。要移动这个光标,我们可以使用UiTranslate(x,y)函数。 示例: function draw(dt) UiTranslate(100,100) UiRect(10,10) end 这段代码将光标从屏幕左上角向右移动100像素,向下移动100像素。然后绘制一个10像素乘10像素的矩形。

如果你将矩形放大,会发现它是从左上角开始缩放的。 如果我们想让矩形在屏幕上居中,这可能会成为一个问题,因为光标会居中,而矩形却从角落开始绘制。 我们可以使用UiAlign(alignment)函数来改变矩形的绘制方式。 function draw(dt) UiAlign("center middle") UiTranslate(100,100) UiRect(10,10) end 你应该会注意到矩形现在发生了偏移,这是因为它现在是从光标的中心开始绘制的。

以下是我们之前的矩形与新居中矩形的对比;红色方块是光标。注意光标本身并未移动,只有正在绘制的元素发生了位置变化。

UiAlign()的所有不同可能对齐方式 现在让我们尝试在屏幕中央绘制一个红色正方形。 首先,我们需要将光标移动到屏幕中心。 UiTranslate(UiCenter(), UiMiddle()) 正如我之前提到的,我们可以使用UiWidth()和UiHeight()来获取屏幕的宽度和高度。 同样,UiCenter()和UiMiddle()可以让我们获取屏幕的中心和中点。这两个函数分别等同于屏幕宽度和高度的一半。 现在让我们更改矩形的绘制方式,使其以光标为中心,而不是从左上角开始绘制。 UiAlign("center middle") UiTranslate(UiCenter(), UiMiddle()) 现在只需绘制矩形并将其颜色设为红色即可。UiAlign("居中 中间") UiTranslate(UiCenter(), UiMiddle()) UiColor(1,0,0) -- UiColor允许我们更改所有将要绘制的元素的颜色,想象一下就像在画图软件中为画笔选择颜色。它使用0到1范围内的RGB颜色值。 UiRect(100,100)

我们成功绘制出了矩形! 现在,如果我们想在坐标(0,0)处绘制第二个蓝色矩形该怎么做呢? 由于UiTranslate()函数用于告知光标在各轴上应移动的像素数,我们需要调用UiTranslate(-UiCenter,-UiMiddle)。但试想一下,如果我们需要在任意位置绘制数百个这样的矩形,那很快就会变得非常繁琐。解决这个问题的方法是使用UiPush()和UiPop()函数来保存光标状态,以便之后可以返回并绘制更多矩形。换句话说,在UiPush和UiPop之间调用的任何平移、对齐或颜色更改,都仅适用于同样位于该Push Pop范围内的元素。示例: UiAlign("left bottom") -- 应用于所有内容,两个矩形都将受到影响 UiPush() UiTranslate(200,200) -- 仅应用于同一Push内的元素 UiColor(1,0,0) UiRect(100,100) UiRect(200,200) UiPop() UiTranslate(1000,1000) -- 不受之前Translate的影响 UiColor(0,1,0) UiRect(100,100) 这是在《拆迁》中制作任何界面的主要概念,只需将光标移动到合适的位置并进行适当的对齐,然后在这些位置绘制元素。 现在我们准备开始构建界面了。 08 从零开始(规划设计) 创建任何东西的第一步都是进行全面规划,这样我们就知道在代码中需要考虑哪些事项。我们将为一款虚构的枪械模组创建一个选项菜单。该选项必须包含【声音】选项卡、【游戏玩法】选项卡和【图形】选项卡。我们希望该菜单能在关卡中打开,并且能够处理切换开关和数值滑块。 为了提升用户体验,我们应设置一个让用户点击以确认更改的按钮,这意味着我们还需要一个常规操作按钮。 明确目标后,我开始构思并设计出了这个简洁的方案。
在风格方面,我们将采用玻璃拟态设计,因为这在设计领域是个热门话题,而且与《拆迁》这款游戏非常契合。
玻璃态设计示例 相信我,随着我们的不断完善,它会变得更好看 09 构建框架 打开你的模组管理器,右键点击本地模组,然后创建一个新的全局模组

现在选择你新创建的模组,然后点击模组管理器底部的文件夹路径,你应该会看到模组文件夹被打开。 用任意文本编辑器打开main.lua文件,我将使用Visual Studio Code。 你应该会看到一个类似这样的默认脚本: function init() end function tick() end function draw() end 我们将只使用draw函数,因为它负责绘制用户界面。 构建主容器 首先,我们必须创建一个主容器,用于容纳主窗口和分类选择器。 我们将使用Push Pop来限制样式,以避免未来出现任何问题。 然后我们将定义一个新的渲染上下文,它将代表我们的容器。--主窗口 UiPush() UiAlign("center middle") --确保窗口在光标中心 UiTranslate(UiCenter(),UiMiddle()) UiWindow(UiWidth() * 0.6, UiHeight() * 0.7) UiPop() 这段代码能运行!但从技术上讲……我们什么也看不见,因为UIWindow不是可见元素;要能看到它,我们需要绘制一个填充整个渲染上下文的矩形。 我编写了一个小调试函数来实现这一点 ---调试函数,用纯色填充当前窗口上下文作为可视化指南。 ---@param red number 默认值为0.0 ---@param green number 默认值为0.0 ---@param blue number 默认值为0.0 ---@param alpha number 默认值为10 函数 UiDebugFill(red,green,blue,alpha) UiPush() UiAlign("center middle") UiTranslate(UiCenter(),UiMiddle()) UiColor(red,green,blue,alpha) UiRect(UiWidth(),UiHeight()) UiPop() 结束 要使用此函数,请在你想要可视化的UiWindow之后立即调用它,如下所示: --主窗口 UiPush() UiAlign("center middle") --确保窗口在光标中心 UiTranslate(UiCenter(),UiMiddle()) UiWindow(UiWidth() * 0.6, UiHeight() * 0.7) UiDebugFill(1,0,0,0.25) UiPop()

创建右侧主窗口并进行样式设计 现在让我们来创建右侧主窗口,并对其进行样式设计!
和之前一样,我们定义一个新的渲染上下文。 我们将使其高度与主容器相同,宽度约为主容器的70%。 我们还会将窗口对齐到“右中”位置,并将其放置在容器的右侧。 为了简化操作,我们定义一个名为UiRightPanel()的函数,该函数将包含面板的代码。这样代码会更简洁,指南也更容易理解。 function UiRightPanel() UiPush() UiAlign("right middle") UiTranslate(UiWidth(),UiMiddle()) -- 定位到全宽,因为我们将其对齐到右侧,所以它会向左扩展,无论如何都不会超出边界。 UiWindow(UiWidth() * 0.8, UiHeight()) UiDebugFill(0,1,0,0.25) UiPop() end function draw() --主窗口 UiPush() UiAlign("center middle") --确保窗口在光标中央 UiTranslate(UiCenter(),UiMiddle()) UiWindow(UiWidth() * 0.6, UiHeight() * 0.7) UiDebugFill(1,0,0,0.25) --右侧面板(我们正在制作这个) UiRightPanel() --左侧面板 UiPush() --这里将是另一个面板 UiPop() UiPop() end 最终效果应与此一致
现在我们可以开始设计右侧面板的样式了。之后,我们将创建一个单独的选项条目。 注释掉所有的DebugFills,这样我们就能清晰地看到所有内容。 我们采用玻璃态设计风格,其基础是使用UiBlur来模糊窗口后方的内容。为了让它更有玻璃质感,我们可以为其添加边框,并将背景设置为略微不透明。 要为元素添加边框,我们可以使用UiRectOutline()。它的工作方式与UiRect完全相同,只是它不绘制填充矩形,而是只绘制矩形的轮廓。我将改用UiRoundedRectOutline(),顾名思义,它具有圆角。我发现圆角矩形(RoundedRects)启用了抗锯齿功能,这使得它们在制作倾斜或旋转的用户界面(Ui)时非常有用。 函数UiRightPanel() UiPush() UiAlign("right middle") UiTranslate(UiWidth(),UiMiddle()) UiWindow(UiWidth() * 0.8, UiHeight(),true) --确保启用裁剪!这能确保只有窗口后面的内容会被模糊,而不是整个屏幕 --模糊窗口后面的所有内容 UiBlur(16) --背景 UiPush() UiAlign("center middle") UiTranslate(UiCenter(),UiMiddle()) UiColor(0,0,0,0.2) UiRoundedRect(UiWidth(),UiHeight(),8) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),8,2) -- 最后一个数字是我们轮廓的厚度 UiPop() -- UiDebugFill(0,1,0,0.25) UiPop() end Result:
如你所见,我们有一个非常漂亮的玻璃面板。 现在我们可以为左侧面板创建一个函数,并将代码复制粘贴到其中,只需更改窗口大小,使其成为一个垂直工具栏。 为了便于日后调整,我们将使用数学计算两个窗口之间的间距,计算公式是用主容器的大小减去右侧窗口的大小,这样我们就能得到剩余空间,再从中减去我们想要的间距量。 首先,我们需要获取第一个窗口的宽度 function UiRightPanel() UiPush() UiAlign("right middle") UiTranslate(UiWidth(),UiMiddle()) UiWindow(UiWidth() * 0.8, UiHeight(),true) -- 确保启用裁剪!这确保只有窗口后方的内容会被模糊,而不是整个屏幕 local w = UiWidth() --模糊窗口后方的所有内容 UiBlur(16) --背景 UiPush() UiAlign("center middle") UiTranslate(UiCenter(),UiMiddle()) UiColor(0,0,0,0.2) UiRoundedRect(UiWidth(),UiHeight(),8) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),8,2) --最后一个数字是轮廓的厚度 UiPop() -- UiDebugFill(0,1,0,0.25) UiPop() return w end 然后我们将结果存入局部变量,并传递给左侧面板函数 --右侧面板(我们正在制作这个) local width = UiRightPanel() --左侧面板 UiPush() UiLeftPanel(width,16) UiPop() function UiLeftPanel(width,gap) UiPush() UiAlign("right middle") UiTranslate(UiWidth() - width - gap,UiMiddle()) UiWindow(UiWidth() * 0.1, UiHeight(),true) --模糊窗口后方的所有内容 UiBlur(16) --背景 UiPush() UiAlign("center middle") UiTranslate(UiCenter(),UiMiddle()) UiColor(0,0,0,0.2) UiRoundedRect(UiWidth(),UiHeight(),8) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),8,2) -- 最后一个数字是轮廓的厚度 UiPop() -- UiDebugFill(0,1,0,0.25) UiPop() end Result:
10a 右侧面板与动态内容填充 我们已完成框架搭建,现在可以专注于选项条目。作为UI设计师和脚本编写者,我们的目标是通过避免手动定义每个条目的方式来减少工作量。相反,我们将使用动态内容填充来实现这一过程的自动化。 动态内容填充能让我们根据特定数据和参数自动为网站或应用填充内容。此方法会用可用选项和输入字段填充右侧面板。它使单个选项元素能够自动调整,因此如果我们之后决定更改选项,无需更新界面。 选项条目 让我们首先创建选项条目本身。创建一个名为UiOptionEntry()的新函数,并将其放入右侧窗口中,这样我们就能看到结果。 我会再次复制粘贴玻璃面板的代码,将其用作元素的基础。 (这里可以做一个改进:将玻璃面板的代码提取到一个函数中。这将减少所需的代码量,提高可读性。) 我还下载了一种合适的字体(Saira),你也可以使用默认的内置字体,但我们的目标是做出美观的效果。 function UiOptionEntry() UiPush() UiAlign("center top") UiTranslate(UiCenter(),UiMiddle()) UiWindow(UiWidth() * 0.9, UiHeight() * 0.1) --背景 UiPush() UiAlign("居中 中间") UiTranslate(UiCenter(),UiMiddle()) UiColor(0.5,0.5,0.5,0.3) UiRoundedRect(UiWidth(),UiHeight(),18) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),18,2) --最后一个数字是轮廓的厚度 UiPop() --文本 UiPush() UiAlign("左 中间") UiTranslate(24,UiMiddle()) UiFont("font/Saira-Regular.ttf",42) UiText("测试选项文本") UiPop() UiPop() end 我将此函数放入右侧面板中 这就是该函数应有的样子。function UiRightPanel() UiPush() UiAlign("右中") UiTranslate(UiWidth(),UiMiddle()) UiWindow(UiWidth() * 0.8, UiHeight(),true) -- 确保启用裁剪!这能保证只有窗口后方的内容会被模糊,而非整个屏幕 local w = UiWidth() --模糊窗口后方的所有内容 UiBlur(16) --背景 UiPush() UiAlign("居中") UiTranslate(UiCenter(),UiMiddle()) UiColor(0,0,0,0.2) UiRoundedRect(UiWidth(),UiHeight(),8) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),8,2) -- 最后一个数字是轮廓的厚度 UiPop() -- UiDebugFill(0,1,0,0.25) UiOptionEntry() UiPop() return w end 最终你应该得到类似这样的结果:
制作切换开关 接下来,我们将准备切换开关。我们会采用一种非常流行的设计,其外观大致如下:

制作起来非常简单,而且看起来很棒!你可以制作一些替代方案,比如复选框或切换按钮。 让我们快速讨论一下自定义切换按钮背后的逻辑。 自定义切换按钮的工作原理如下:切换功能会检测用户何时用鼠标点击它。当检测到点击时,我们会在内部翻转变量的状态,然后更新切换按钮的外观以反映这一变化。

在《拆迁》中检测鼠标点击,我们可以使用函数InputPressed("lmb")。但同时,我们还需要确保只有当鼠标悬停在开关上时才会注册点击,这可以通过函数UiIsMouseInRect()来实现。将这两个函数结合使用,就能有效检测特定区域内的点击。 最后一步是使用UiTranslate()来设置可点击区域的位置和大小,使其与开关对齐。然后,我们将开关切换函数与点击检测关联起来,以便从该点执行剩余代码。 最佳实践是为输入元素(如开关或滑块)定义固定大小,这样可以确保它们不会意外拉伸,从而避免不必要的问题。我选择了88×44像素的尺寸。

function UiToggleSwitch() UiAlign("右中") UiTranslate(UiWidth() - 24, UiMiddle()) UiWindow(88, 44) --背景 UiPush() UiAlign("居中") UiTranslate(UiCenter(),UiMiddle()) UiColor(0.1,0.1,0.1,0.25) UiRoundedRect(UiWidth(),UiHeight(),22) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),22,2) --最后一个数字是轮廓的厚度 UiPop() --滑块 UiPush() local thumb_size = 38 UiAlign("左中") UiTranslate(3,UiMiddle()) UiColor(0.5,0.5,0.5,0.8) UiRoundedRect(thumb_size,thumb_size,20) UiColor(1,1,1,0.15) UiRoundedRectOutline(thumb_size,thumb_size,20,2) -- 最后一个数字是轮廓的粗细 UiPop() end 我们有一个很棒的开关。下一步是编写逻辑,以及滑块的颜色变化和移动。 function UiToggleSwitch() UiAlign("right middle") UiTranslate(UiWidth() - 24, UiMiddle()) UiWindow(88, 44) --逻辑 UiPush() UiAlign("left top") local clicked = UiIsMouseInRect(UiWidth(),UiHeight()) and InputPressed("lmb") if clicked then DebugPrint("我被点击了!") end UiPop() [...] -- 其余代码未更改 end

此代码能让我们检测到切换按钮上的鼠标点击。不过,由于鼠标正在控制相机,我们会遇到无法移动鼠标的问题。要解锁鼠标,我们可以使用UiMakeInteractive()函数。像这样在绘制回调的开头添加此函数: 现在,我们将准备处理选项条目/输入以及内容填充所需的表格结构。由于我们已经确定需要设置不同类别的选项,因此我们将构建如下结构: function init() Options = { gameplay = { {title = "无限弹药", value = false}, {title = "爆炸子弹", value = false}, {title = "无敌模式", value = false} }, graphics = { {title = "画质", value = 1}, {title = "粒子效果", value = false}, {title = "阴影", value = false} }, sound = { {title = "主音量", value = 1}, {title = "音乐音量", value = 1}, }, miscellanous = { -- 目前留空 } } end 我们有一个中央选项表,按指定类别组织所有选项条目。我们将遍历此表,并根据其包含的数据生成每个条目。首先,我们来完成切换开关,然后用所有条目填充它。我们可以加载第一个条目的数据作为参考。 10b 动态内容填充与内容溢出处理 function UiToggleSwitch(current_entry) UiAlign("right middle") UiTranslate(UiWidth() - 24, UiMiddle()) UiWindow(88, 44) --逻辑 UiPush() UiAlign("left top") local clicked = UiIsMouseInRect(UiWidth(),UiHeight()) and InputPressed("lmb") if clicked then --值切换 current_entry.value = not current_entry.value end UiPop() --背景 UiPush() --着色 if current_entry.value == true then UiColor(0, 0.7, 0.184,0.4) else UiColor(0.9, 0, 0,0.4) end UiAlign("center middle") UiTranslate(UiCenter(),UiMiddle()) -- UiColor(0.1,0.1,0.1,0.25) UiRoundedRect(UiWidth(),UiHeight(),22) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),22,2) -- last number is the thickness of our outline UiPop() --Thumb UiPush() local thumb_size = 38 UiAlign("left middle") --Movement if current_entry.value == true then UiTranslate(UiCenter(),0) end UiTranslate(3,UiMiddle()) UiColor(0.5,0.5,0.5,0.8) UiRoundedRect(thumb_size,thumb_size,20) UiColor(1,1,1,0.15) UiRoundedRectOutline(thumb_size,thumb_size,20,2) -- last number is the thickness of our outline UiPop() end


我们正在传递一个指向入口的指针,这样在必要时只需更改选项入口函数中的变量,就能轻松替换该入口。

目前尚未实现任何动画效果,我会在指南的最后部分处理这一问题。 动态填充 现在,我们可以开始向面板中填充所有条目了。首先,我们需要创建一个新的渲染上下文,作为所有条目的容器。这种分离处理将便于我们后续添加滚动功能,并确保元素不会超出窗口范围。

--我们的条目列表 UiAlign("left top") UiWindow(UiWidth(),UiHeight()) UiDebugFill(1,0,0,0.5) UiOptionEntry(Options.gameplay[1]) 运行代码后,你会注意到现在有了一个新的渲染上下文,它会填满整个窗口。你可以移除DebugFill,因为它不再需要了。 为了正确定位条目,我们需要将转换移出函数,并放在调用函数之前。



我会在我们的第一个条目之前添加一个UiTranslate,将其向下移动一点作为间距。

现在我们将所有内容放入一个 for 循环中,该循环会遍历所有条目,并将每个条目向下偏移。 要计算每个条目的位置,我们取当前条目的索引并将其乘以条目的高度。我们还可以在这个高度上加上一个小值,以便在条目之间创建一些间距。

UiPush() UiAlign("居中 顶部") UiTranslate(UiCenter(),32) local entry_height = UiHeight() * 0.1 local gap = 16 local category = Options.gameplay for index, entry in ipairs(category) do --因为索引从1开始,所以需要减1使其从0开始 --这样第一个元素就不会偏移 local offset = (index-1) * (entry_height + gap) UiPush() UiTranslate(0,offset) UiOptionEntry(category[index],entry_height) UiPop() end UiPop()


现在我们可以回过头来快速处理,让它加载条目的标题而不是占位文本。 处理内容溢出和动态滚动条 要判断内容是否超出可见区域,我们需要将所有显示条目的高度(包括条目之间的间距)相加。如果这个总高度超过了渲染上下文的高度,我们就可以判定内容发生了溢出,此时应该显示滚动条。 由于我们知道表格的长度以及每个条目的大小,因此只需一行代码就能完成这个计算!

以下是滚动条的代码 function UiScrollbar() UiAlign("右中") UiTranslate(UiWidth() - 6,UiMiddle()) UiWindow(10,UiHeight() - 12) local thumb_length = 90 --背景 UiPush() UiAlign("居中") UiTranslate(UiCenter(),UiMiddle()) UiColor(0,0,0,0.2) UiRoundedRect(UiWidth(),UiHeight(),5) UiPop() --滑块 UiPush() UiAlign("居中") UiTranslate(UiCenter(),UiMiddle()) UiColor(1,1,1,0.3) UiRoundedRect(UiWidth(),thumb_length,5) UiColor(1,1,1,0.5) UiRoundedRectOutline(UiWidth(),thumb_length,5,1) UiPop() end

滚动条初始状态应为隐藏。当表格中的条目数量增加到足以使窗口溢出时,滚动条将变为可见。

如你所见,当内容溢出时,滚动条会出现。我们需要解决的最后一个问题是防止内容超出其边界。 要解决此问题,请将渲染上下文中的剪辑值设置为true,并确保选项条目的继承值也设置为true。


最后,我们将确保滚动条能够随内容自动滚动和调整大小。它将响应滚轮操作和拖动滑块的操作。 温馨提示,代码非常长。 1. 定义两个新变量【ScrollAmount】和【ScrollTranslate】,这两个变量将用于存储滚动位置以及窗口的计算平移量。

传递总和值,以便我们用它来计算滚动条的大小和位置。

3. 将所有数学运算粘贴到滚动条的滑块中 10c 完成滚动条 函数 UiScrollbar(sum) UiAlign("右中") UiTranslate(UiWidth() - 6, UiMiddle()) UiWindow(10, UiHeight() - 12) --背景 UiPush() UiAlign("居中") UiTranslate(UiCenter(), UiMiddle()) UiColor(0,0,0,0.2) UiRoundedRect(UiWidth(), UiHeight(),5) UiPop() --滑块 UiPush() --计算高度 局部变量 trackHeight = UiHeight() 局部变量 thumbHeight = (UiHeight()/sum) * trackHeight UiAlign("中上") --检测滑块按压 UiPush() UiAlign("左上") UiTranslate(0,math.floor(scrollAmount)) local scrollHover = UiIsMouseInRect(UiWidth(), thumbHeight) UiPop() if scrollHover then if InputPressed("lmb") then scrollbarPressed = true end UiDebugFill(1,1,1,1) end if InputReleased("lmb") then scrollbarPressed = false end -- Only scroll when not pressed down if not scrollbarPressed and not inputActive then local scroll = InputValue("mousewheel") if scroll ~= 0 then scrollAmount = scrollAmount - scroll*30 end end --Mouse thumb to position of the mouse if scrollbarPressed then local x,y = UiGetMousePos() scrollAmount = y - thumbHeight*0.5 end --定位和限制 local s = UiHeight() - thumbHeight if scrollAmount > s then scrollAmount = s elseif scrollAmount < 0 then scrollAmount = 0 end --滑块 UiTranslate(UiCenter(),math.floor(scrollAmount)) UiColor(1,1,1,0.3) UiRoundedRect(UiWidth(),thumbHeight,5) UiColor(1,1,1,0.5) UiRoundedRectOutline(UiWidth(),thumbHeight,5,1) UiPop() local scrollScaled = (scrollAmount/trackHeight) * sum scrollTranslate = math.floor(-scrollScaled) end 然后我们只需在UiWindow之前添加一个UiTranslate来根据需要偏移它。
搞定!我们已经完成了右侧面板!现在让我们继续处理左侧边栏。 11a 为左侧面板创建图标按钮 我们将首先创建一个名为UiIconButton()的新函数。此函数将生成一个中心带有图标的按钮,点击该按钮时会切换活动的选项标签页。 要切换标签页,我们可以更改在内容填充期间创建的currentCategory变量。当点击按钮时,我们会检查函数的category参数,并相应地更新该变量。 我将再次复制粘贴glasspane代码,并以此为基础开始操作:

function UiIconButton(icon,category,button_size) UiPush() UiWindow(button_size, button_size) --背景 UiPush() UiAlign("center middle") UiTranslate(UiCenter(),UiMiddle()) UiColor(0.5,0.5,0.5,0.3) UiRoundedRect(UiWidth(),UiHeight(),18) UiColor(1,1,1,0.15) UiRoundedRectOutline(UiWidth(),UiHeight(),18,2) --最后一个数字是轮廓的厚度 UiPop() UiPop() end 让我们用与右侧面板相同的方式填充导航栏,但不是读取选项条目,而是读取分类。

--填充按钮 本地按钮尺寸 = 界面宽度() * 0.6 本地间距 = 16 界面平移(界面中心(),32) 界面对齐("居中靠上") 本地索引 = 0 对于 键, 分类 在 选项 中 循环执行 界面入栈() 本地偏移量 = 索引 * (按钮尺寸 + 间距) 界面平移(0,偏移量) 界面图标按钮(图标,分类,按钮尺寸) 界面出栈() 索引 = 索引 + 1 结束循环 如你所见,我们使用了一个单独的迭代器变量,因为我们使用的是pairs循环而非ipairs循环。原因是我们的选项表是一个键控表,其中的条目包含诸如"gameplay"、"sound"等键。因此,为了获取当前数量,我们需要使用一个单独的变量。注意:键控表没有定义顺序,这意味着它们在每个用户那里会以不同顺序显示。解决此问题的方法是改用索引表。结果:

现在我们可以为按钮添加图标了。出于风格考虑,添加图标后我会移除按钮的背景。这只是个人偏好而已。

--图标 界面推送() 界面对齐("居中 中间") 界面平移(界面中心(),界面中间()) 界面图像("") 界面弹出()
UIImage()的一个重要特性是,默认情况下它会以图像的原始尺寸显示。这意味着如果你插入一个1000像素×1000像素的图标,它会填满整个屏幕! 为了解决这个问题,我们需要将图标调整为固定的预定义尺寸。这可以通过UiScale()来实现。UiScale允许我们按一个从0开始的系数缩放任何UI元素。 缩放系数为0.5表示原始尺寸的一半,1表示原始尺寸,2表示原始尺寸的两倍,依此类推。 要设置特定的像素大小(例如64像素),我们需要将其转换为图像尺寸的百分比。这可以通过将所需尺寸除以原始图像的尺寸来完成。 示例:64/1024=0.0625。如果我们将图像缩放到其尺寸的0.0625%,最终尺寸将恰好为64像素。代码和最终结果:
--图标 UiPush() UiAlign("居中 中间") UiTranslate(UiCenter(),UiMiddle()) UiScale(48/UiGetImageSize(icon)) UiImage(icon) UiPop() 让我们使用Inkscape为按钮创建一些图标。 我不会提供在Inkscape中创建所有图标的详细说明。相反,我将演示如何制作我们需要的其中一个图标。正如我在理论部分前面提到的,图标应代表每个按钮的功能或指示其指向的位置。 我们将创建: - 一个游戏手柄图标,代表游戏玩法设置。 - 一个显示器图标,代表图形设置。 - 一个扬声器图标,代表音频设置。 - 一个齿轮图标,代表其他设置。 这些图标虽然通用,但对于本指南来说已经足够了!我将展示如何制作齿轮,因为它是最简单的。 11b 使用Inkscape(简化版) 欢迎使用Inkscape! 界面一开始可能会有点令人困惑,但别担心,这是一款超级简单但功能强大的应用程序。

1. 按下磁铁图标可启用吸附功能,这会非常有用。

2. 选择圆形工具并绘制两个居中的圆形,使其形成类似甜甜圈的形状。你可以使用Shift+Ctrl组合键从中心绘制正圆形。

3.选择矩形工具,在画布任意位置绘制一个小矩形,我们将用它来制作齿轮的齿。完成后,转到右侧面板并打开路径效果选项卡。

4. 现在添加“旋转复制”效果,双击矩形并选择中心节点,将其移动到圆形的中心,然后抓住矩形并将其移到较大圆形的正上方。 你可以根据需要增加复制数量,我会将其设置为8个。

5. 现在我们要将所有形状合并在一起。选择大圆形和矩形,然后按键盘上的X键。这将启用形状生成工具。接着按住鼠标并拖动,覆盖所有形状,使它们在发出蓝光时合并在一起。最后按Enter键。 然后选择你的新形状和小圆形,这次按住Shift键点击小圆形,然后点击外部的圆形,按Enter键。

6. 最后,点击斜角效果按钮并增加斜角量,这将使图标的边缘变得平滑。然后将图标的颜色更改为白色,之后进行导出。

7. 将图标导出到你的模组根文件夹
这是基础部分,接下来我会制作其余所需的图标。如果你不想麻烦,可以直接从谷歌字体下载一些图标。 11c 完成图标按钮。 现在我们只需为每个按钮分配正确的图标。将图标文件命名为与相应类别匹配的名称,我们将使用这些名称来选择合适的文件。
--填充按钮 本地按钮尺寸 = 界面宽度() * 0.6 本地间距 = 16 界面平移(界面中心(),32) 界面对齐("居中靠上") 本地索引 = 0 对于 键, 类别 在 选项 中 成对循环 做 界面推入() 本地偏移量 = 索引 * (按钮尺寸 + 间距) 界面平移(0,偏移量) 本地图标 = 键 .. ".png" 界面图标按钮(图标,类别,按钮尺寸) 界面弹出() 索引 = 索引 + 1 结束

我们要做的最后一件事是为按钮添加逻辑。首先,我们需要定义一个变量来保存当前选中的标签页,然后用新的变量替换临时变量。

最后,我们将检测按钮上的点击,然后更改变量所保存的类别。
UiAlign("left top") local clicked = UiIsMouseInRect(UiWidth(),UiHeight()) and InputPressed("lmb") if clicked then currentCategory = category end 这应该就是结果!
12 最终润色(音效、悬停效果和简单动画) 音效 让我们快速为我们的用户界面创建一些音效,我将使用Vital和LMMS。 打开LMMS后,你应该会看到默认的项目文件。

1. 点击齿轮图标,然后选择移除,删除所有乐器。

2. 从左侧菜单中拖放Vestige插件,然后点击乐器图标,再点击绿色文件夹图标。从你的硬盘中选择Vital VST插件。

3. 应该会出现Vital的窗口;复制这些设置。使用屏幕底部的键盘来播放不同的声音。你会注意到它会播放许多不同的、独特的声音。如果你什么都听不到,或者不喜欢你当前的声音,可以按下白色骰子图标来随机化AMP滤波器。
4. 找到喜欢的声音后,返回LMMS并双击乐器钢琴卷帘,然后绘制与你的音效对应的音符即可。

5. 接下来只需导出声音文件。点击左上角的“文件”>“导出”。然后选择你的模组根文件夹,格式选择OGG,并将文件命名为click.ogg。注意:确保勾选“导出为循环”选项。

现在让我们为按钮和开关添加音效。

UiAlign("left top") local clicked = UiIsMouseInRect(UiWidth(),UiHeight()) and InputPressed("lmb") if clicked then UiSound("click.ogg",1) currentCategory = category end 对我们的切换执行相同操作。

鼠标悬停音效 要实现鼠标悬停音效,我们需要检测鼠标是否进入元素,然后播放音效,之后切换防抖动状态以防止多次播放音效。 我们可以使用注册表来保存状态。 以下是一个实用的函数: function UiMouseOverSound(sound, volume, index) local key = "level.ui.mouseover" .. index local debounce = GetBool(key) if UiIsMouseInRect(UiWidth(),UiHeight()) then if not debounce then UiSound(sound,volume) SetBool(key,true) end else SetBool(key,false) end end 你需要在进行内容填充的for循环中为该函数提供索引值。

现在对图标按钮执行相同操作,你就能为自己添加一些鼠标悬停音效了! 注意:添加鼠标悬停音效时,记得为索引添加一些数字,确保它与其他元素的索引不同。 悬停高亮和点击高亮 我们可以通过为用户提供其悬停元素的交互反馈来增强用户界面(UI)。 这可以通过在鼠标悬停在交互元素上时突出显示它们来实现。我们可以使用UiMouseIsInRect()函数检查鼠标是否在元素边界内,然后应用UiColorFilter()来突出显示它。 此外,对于鼠标点击,我们可以应用颜色滤镜来指示元素何时被点击。


动画 《拆迁》用户界面中的动画并不常见;事实上,它相对较新,主要原因是应用程序接口缺乏用于轻松制作用户界面元素动画的原生函数。 在过去的几个月里,我一直在开发一个库来解决这个问题。该库包含一个完整的补间系统,设计得既简单又强大。 我将首先演示如何使用补间的基本实现来制作用户界面元素的动画,然后展示如何使用我的库来实现这一点。 我计划很快发布它。虽然它还没准备好,但我认为值得一提,特别是因为很多人都对《Volatile Blackholes》缺乏更新表示担忧,这主要是由于这个项目的缘故。 我们将只对切换开关进行线性动画处理。这意味着无法进行缓动处理,因为这需要创建一整套补间系统,而我们没有时间去做。 最简单的物体动画方法是不断将起始值向目标值进行插值,也就是说,每一帧它都会逐渐向目标值靠近。 这是一种反模式,意味着在正式项目中应该避免使用。我分享这个只是因为我觉得这是一个有趣的技巧,值得了解。但请注意,这仅仅是个技巧。 如果你想创建动画,最好使用合适的补间系统,比如Neohud。

--移动 本地变量 key = "level.ui.switchanimation".. 索引 ..".progress" 本地变量 val = 获取浮点值(key) 本地变量 持续时间 = 0.1 本地变量 t = 时间增量/持续时间 如果 当前条目.值 == 真 则 本地变量 x = 线性插值(val,界面中心(),t) 设置浮点值(key,x) 界面平移(x,0) 否则 本地变量 x = 线性插值(val,0,t) 设置浮点值(key,x) 界面平移(x,0) 结束
13 尾声 感谢你阅读本指南。我花了很多时间撰写它,因为我想分享一些在《拆迁》近一年的使用经历中获得的知识。 如果本指南反响良好,我将继续创作另一篇专注于脚本主题的指南。

关于本指南,如有任何问题欢迎提出。爱你的,Novena <3
2026-02-14 13:00:09 发布在
Teardown
说点好听的...
收藏
0
0
