[{"content":"在 Windows 和 Arch Linux 上，让 Caps Lock 键变成中英文切换键 用惯了 Mac 之后，再回到 Windows 或 Linux 电脑上，总有些小地方让人感觉不太适应。对我来说，其中一个就是键盘上的 Caps Lock 键。 在 macOS 上，Caps Lock 键默认可以用来快速切换中英文输入法，这个设计非常直观，也减少了手指的移动距离。但在 Windows 和大多数 Linux 发行版里，这个键通常只负责大小写锁定，一个功能对我来说使用频率并不高。于是，我便开始琢磨，能不能在这两大主流操作系统上也实现类似的功能。\n经过一番探索，我找到了解决方案。在 Windows 上，我们可以使用 AutoHotkey；而在 Arch Linux 上，我选择的是Keyd。 Windows 篇：使用 AutoHotkey AutoHotkey 是一款免费且功能强大的自动化脚本软件，可以通过编写脚本来定制各种键盘快捷键。我们只需要用到它最基础的一点功能。 准备工作 首先，你需要从 AutoHotkey 的官网（https://www.autohotkey.com/）下载并安装它。安装时选择默认选项即可。 实现 Caps Lock 切换中英文 这个脚本不仅能让单击 Caps Lock 切换中英文，还保留了长按切换大小写的功能。 在使用前，请确保你的 Windows 系统默认的中英文切换快捷键是 Ctrl + Space。如果不是，可以在“设置”-\u0026gt;“时间和语言”-\u0026gt;“语言和区域”-\u0026gt;“输入”-\u0026gt;“高级键盘设置”中进行修改。 脚本内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #Requires AutoHotkey v2.0 #SingleInstance Force ; 让脚本在后台持续运行 Persistent global isCapsPressed := false CapsLock:: { if (KeyWait(\u0026#34;CapsLock\u0026#34;, \u0026#34;T0.5\u0026#34;)) { if(!isCapsPressed){ ; 如果是单击，则发送 Ctrl+Space 来切换中英文 Send \u0026#34;^{Space}\u0026#34; } global isCapsPressed := false } else { if(!isCapsPressed){ global isCapsPressed := true ; 如果是长按（超过0.5秒），则切换大小写锁定状态 SetCapsLockState !GetKeyState(\u0026#34;CapsLock\u0026#34;, \u0026#34;T\u0026#34;) } } } 如何使用这个脚本：\n在电脑的任意位置新建一个文本文档。\n将上面的代码复制并粘贴到这个文本文档中。\n保存文件，然后将文件名从 .txt 修改为 .ahk，例如 SwitchIME.ahk。\n双击运行这个 .ahk 文件。\n设置开机自启动 每次开机都手动双击运行脚本会比较麻烦，我们可以把它设置成开机自动启动。\n打开文件资源管理器。\n在顶部的地址栏输入 shell:startup，然后按回车键。这时会打开 Windows 的“启动”文件夹。\n将你刚才创建的那个 .ahk 脚本文件，复制或剪切到这个文件夹里。\nArch Linux 篇：使用 keyd 在 Arch Linux 上，我选择的是keyd，为什么是这个，没啥原因，只是选了这个:3 准备工作：安装 keyd 在 Arch Linux 上，安装 keyd 非常简单，直接使用 pacman 即可。\n1 sudo pacman -S keyd 安装完成后，需要启用并启动 keyd 服务（这里是设置为开机自启和现在就启动）：\n1 sudo systemctl enable --now keyd 配置 keyd 实现 Caps Lock 切换 keyd 的配置文件位于 /etc/keyd/ 目录下，例如 default.conf，这是keyd的默认配置。\n1 sudo nano /etc/keyd/default.conf 将以下内容粘贴到文件中：\n1 2 3 4 5 6 7 8 [ids] * [main] # 将 Caps Lock 配置为： # - 短按：作为一次性的Ctrl+空格组合键 # - 长按 500ms：切换大小写锁定状态 capslock = timeout(macro(C-space),500,capslock) 配置详解：\n因为我是用的fcitx5作为输入法，然后它默认是Ctrl+空格作为输入法切换，然后只要把这个shift的按键绑定取消在加个这个就可以实现我们想要的效果了，下面是这条命令的解释。\ntimeout(\u0026lt;动作1\u0026gt;, \u0026lt;超时时间\u0026gt;, \u0026lt;动作2\u0026gt;)\n如果该按键被单独按住超过 \u0026lt;超时时间\u0026gt; 毫秒，则激活第二个动作；如果按住时间少于 \u0026lt;超时时间\u0026gt; 毫秒，或在 \u0026lt;超时时间\u0026gt; 到期前按下另一个键，则执行第一个动作。\n应用配置并验证 保存并关闭文件后，需要重新加载 keyd 配置使其生效：\n1 sudo keyd reload 现在，你可以立即测试一下：\n单击 Caps Lock，应该会切换输入法。 长按 Caps Lock 超过 500 毫秒，应该会切换大小写锁定。 ","permalink":"https://yulan233.github.io/posts/%E5%9C%A8win%E5%92%8Carchlinux%E4%B8%8A%E8%AE%A9caps-lock%E9%94%AE%E5%8F%98%E6%88%90%E4%B8%AD%E8%8B%B1%E6%96%87%E5%88%87%E6%8D%A2%E9%94%AE/","summary":"\u003ch3 id=\"在-windows-和-arch-linux-上让-caps-lock-键变成中英文切换键\"\u003e在 Windows 和 Arch Linux 上，让 Caps Lock 键变成中英文切换键\u003c/h3\u003e\n\u003cp\u003e用惯了 Mac 之后，再回到 Windows 或 Linux 电脑上，总有些小地方让人感觉不太适应。对我来说，其中一个就是键盘上的 Caps Lock 键。\n在 macOS 上，Caps Lock 键默认可以用来快速切换中英文输入法，这个设计非常直观，也减少了手指的移动距离。但在 Windows 和大多数 Linux 发行版里，这个键通常只负责大小写锁定，一个功能对我来说使用频率并不高。于是，我便开始琢磨，能不能在这两大主流操作系统上也实现类似的功能。\u003c/p\u003e\n\u003ch2 id=\"经过一番探索我找到了解决方案在-windows-上我们可以使用-autohotkey而在-arch-linux-上我选择的是keyd\"\u003e经过一番探索，我找到了解决方案。在 Windows 上，我们可以使用 AutoHotkey；而在 Arch Linux 上，我选择的是\u003ccode\u003eKeyd\u003c/code\u003e。\u003c/h2\u003e\n\u003ch4 id=\"windows-篇使用-autohotkey\"\u003eWindows 篇：使用 AutoHotkey\u003c/h4\u003e\n\u003cp\u003eAutoHotkey 是一款免费且功能强大的自动化脚本软件，可以通过编写脚本来定制各种键盘快捷键。我们只需要用到它最基础的一点功能。\n\u003cstrong\u003e准备工作\u003c/strong\u003e\n首先，你需要从 AutoHotkey 的官网（\u003ca href=\"https://www.autohotkey.com/\"\u003ehttps://www.autohotkey.com/\u003c/a\u003e）下载并安装它。安装时选择默认选项即可。\n\u003cstrong\u003e实现 Caps Lock 切换中英文\u003c/strong\u003e\n这个脚本不仅能让单击 Caps Lock 切换中英文，还保留了长按切换大小写的功能。\n在使用前，请确保你的 Windows 系统默认的中英文切换快捷键是 \u003ccode\u003eCtrl + Space\u003c/code\u003e。如果不是，可以在“设置”-\u0026gt;“时间和语言”-\u0026gt;“语言和区域”-\u0026gt;“输入”-\u0026gt;“高级键盘设置”中进行修改。\n脚本内容如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-autohotkey\" data-lang=\"autohotkey\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003e#Requires\u003c/span\u003e \u003cspan class=\"n\"\u003eAutoHotkey\u003c/span\u003e \u003cspan class=\"n\"\u003ev2\u003c/span\u003e\u003cspan class=\"o\"\u003e.\u003c/span\u003e\u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"nb\"\u003e#SingleInstance\u003c/span\u003e \u003cspan class=\"n\"\u003eForce\u003c/span\u003e\u003cspan class=\"c1\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e; 让脚本在后台持续运行\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"n\"\u003ePersistent\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"nb\"\u003eglobal\u003c/span\u003e \u003cspan class=\"n\"\u003eisCapsPressed\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nv\"\u003efalse\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"nl\"\u003eCapsLock::\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\u003cspan class=\"n\"\u003eif\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eKeyWait\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;CapsLock\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;T0.5\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\u003cspan class=\"n\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eisCapsPressed\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\u003cspan class=\"c1\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\t\t\t; 如果是单击，则发送 Ctrl+Space 来切换中英文\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\t\u003cspan class=\"nb\"\u003eSend\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;^{Space}\u0026#34;\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\u003cspan class=\"nb\"\u003eglobal\u003c/span\u003e \u003cspan class=\"n\"\u003eisCapsPressed\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nv\"\u003efalse\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e \u003cspan class=\"n\"\u003eelse\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\u003cspan class=\"n\"\u003eif\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"n\"\u003eisCapsPressed\u003c/span\u003e\u003cspan class=\"p\"\u003e){\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\t\u003cspan class=\"nb\"\u003eglobal\u003c/span\u003e \u003cspan class=\"n\"\u003eisCapsPressed\u003c/span\u003e \u003cspan class=\"o\"\u003e:=\u003c/span\u003e \u003cspan class=\"nv\"\u003etrue\u003c/span\u003e\u003cspan class=\"c1\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\t\t\t; 如果是长按（超过0.5秒），则切换大小写锁定状态\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\t\u003cspan class=\"nb\"\u003eSetCapsLockState\u003c/span\u003e \u003cspan class=\"o\"\u003e!\u003c/span\u003e\u003cspan class=\"nf\"\u003eGetKeyState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s\"\u003e\u0026#34;CapsLock\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;T\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\t\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"err\"\u003e\u003c/span\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\u003cspan class=\"err\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e如何使用这个脚本：\u003c/strong\u003e\u003c/p\u003e","title":"在Win和ArchLinux上，让Caps Lock键变成中英文切换键"},{"content":"原理分析 核心概念：三维模型的二维切片 模型切片处理\n将3D模型沿Y轴（垂直方向）切割为N层横截面，每层保存为单独的2D精灵图（Sprite）\n分层渲染机制\n1 2 3 4 5 6 7 for i = 1, layer_count do draw_sprite( position.x + i * offset_x, position.y + i * offset_y, layer[i] ) end 通过逐层叠加渲染，每层施加位移偏移量，模拟Z轴深度\n视觉欺骗原理\n利用人眼的透视错觉，通过各层错位移动产生立体感，配合光源统一处理增强立体效果\n","permalink":"https://yulan233.github.io/posts/%E7%B2%BE%E7%81%B5%E5%9B%BE%E5%A0%86%E5%8F%A02d%E6%B8%B2%E6%9F%93%E4%B8%AD%E7%9A%84%E4%BC%AA3d/","summary":"\u003ch2 id=\"原理分析\"\u003e原理分析\u003c/h2\u003e\n\u003ch3 id=\"核心概念三维模型的二维切片\"\u003e核心概念：三维模型的二维切片\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e模型切片处理\u003c/strong\u003e\u003cbr\u003e\n将3D模型沿Y轴（垂直方向）切割为N层横截面，每层保存为单独的2D精灵图（Sprite）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e分层渲染机制\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e7\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003efor\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003elayer_count\u003c/span\u003e \u003cspan class=\"kr\"\u003edo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003edraw_sprite\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eposition.x\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eoffset_x\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eposition.y\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003ei\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eoffset_y\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003elayer\u003c/span\u003e\u003cspan class=\"p\"\u003e[\u003c/span\u003e\u003cspan class=\"n\"\u003ei\u003c/span\u003e\u003cspan class=\"p\"\u003e]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e通过逐层叠加渲染，每层施加位移偏移量，模拟Z轴深度\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e视觉欺骗原理\u003c/strong\u003e\u003cbr\u003e\n利用人眼的透视错觉，通过各层错位移动产生立体感，配合光源统一处理增强立体效果\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e","title":"精灵图堆叠：2D渲染中的伪3D"},{"content":"在 Roblox 游戏开发领域，高效的工具管理与资源整合是提升开发效率的关键。aftman 工作流凭借其强大的工具管理能力，成为开发者的得力助手。它以 aftman 作为工具管理器（也可以用使用npm，毕竟aftman很久不更新了），统筹 wally 和 rojo 两大工具，前者用于管理第三方库，后者负责同步，搭配 vscode 中的 roblox lsp 插件实现语法补全，还可借助 roblox lsp（knit）插件和 selene 进一步优化语法与错误检测。接下来，就为大家详细介绍具体使用方法。\n安装 aftman 想要开启 aftman 工作流，首先要安装 aftman。前往 GitHub 网站，找到与你系统对应的版本进行下载安装。安装完成后，便可着手创建新项目。在当前文件夹下，通过运行aftman init命令，创建一个新的aftman.toml文件，这是后续管理工具的重要配置文件。 （这部分也以使用npm进行，不过wally好像在npm中没有包，可以自己放入环境变量中，rojo可以直接使用npm进行安装，比aftman方便些）\n添加工具 添加 rojo 添加工具时，以 rojo 为例（==我这里使用的是当前的最新版，现在最新可能不是这个版本==）你可以使用以下命令：\n1 2 3 4 5 6 # 添加最新版本的rojo aftman add rojo-rbx/rojo # 添加指定版本7.5.1的rojo aftman add rojo-rbx/rojo@7.5.1 # 以不同的二进制名称添加工具，例如添加ripgrep并命名为rg aftman add BurntSushi/ripgrep rg 添加 wally 使用 aftman 添加 wally 的流程如下：\n1 2 3 4 5 6 # 初始化aftman配置 aftman init # 添加wally工具 aftman add UpliftGames/wally # 安装已添加的工具 aftman install 配置 wally 在 wally 中添加服务器或客户端的包，需要对配置文件进行设置。在相关配置文件中，通过以下内容进行配置：\n1 2 3 4 5 6 7 8 9 10 [package] name = \u0026#34;11588/one\u0026#34; version = \u0026#34;0.1.0\u0026#34; registry = \u0026#34;https://github.com/UpliftGames/wally-index\u0026#34; realm = \u0026#34;shared\u0026#34; [dependencies] Signal = \u0026#34;sleitnick/signal@^1\u0026#34; Trove = \u0026#34;sleitnick/trove@1.5.0\u0026#34; [server-dependencies] # 这里用于添加服务器中的内容 其中，package部分定义了包的基本信息，dependencies用于添加通用依赖，server-dependencies则专门用于添加服务器端的依赖内容，一般客户端都会放到ReplicatedStorage中，相当于客户端服务端共享，但是不互通。\nrojo 同步 wally 库 完成上述配置后，需要通过 rojo 实现 wally 库的同步。在 rojo 的配置文件中，添加如下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { \u0026#34;name\u0026#34;: \u0026#34;wally test\u0026#34;, \u0026#34;tree\u0026#34;: { \u0026#34;$className\u0026#34;: \u0026#34;DataModel\u0026#34;, \u0026#34;ReplicatedStorage\u0026#34;: { \u0026#34;$className\u0026#34;: \u0026#34;ReplicatedStorage\u0026#34;, \u0026#34;$ignoreUnknownInstances\u0026#34;: true, \u0026#34;Packages\u0026#34;: { \u0026#34;$path\u0026#34;: \u0026#34;Packages\u0026#34; } }, \u0026#34;ServerScriptService\u0026#34;: { \u0026#34;$className\u0026#34;: \u0026#34;ServerScriptService\u0026#34;, \u0026#34;Package\u0026#34;: { \u0026#34;$path\u0026#34;: \u0026#34;ServerPackages\u0026#34; } } } } 通过这样的配置，rojo 能够将 wally 管理的第三方库同步到 Roblox 游戏项目中。\n","permalink":"https://yulan233.github.io/posts/roblox-%E6%B8%B8%E6%88%8F%E5%BC%80%E5%8F%91%E4%B8%AD%E4%BD%BF%E7%94%A8vscode/","summary":"\u003cp\u003e在 Roblox 游戏开发领域，高效的工具管理与资源整合是提升开发效率的关键。aftman 工作流凭借其强大的工具管理能力，成为开发者的得力助手。它以 aftman 作为工具管理器（\u003cstrong\u003e也可以用使用npm，毕竟aftman很久不更新了\u003c/strong\u003e），统筹 wally 和 rojo 两大工具，前者用于管理第三方库，后者负责同步，搭配 vscode 中的 roblox lsp 插件实现语法补全，还可借助 roblox lsp（knit）插件和 selene 进一步优化语法与错误检测。接下来，就为大家详细介绍具体使用方法。\u003c/p\u003e\n\u003ch2 id=\"安装-aftman\"\u003e安装 aftman\u003c/h2\u003e\n\u003cp\u003e想要开启 aftman 工作流，首先要安装 aftman。前往 GitHub 网站，找到与你系统对应的版本进行下载安装。安装完成后，便可着手创建新项目。在当前文件夹下，通过运行aftman init命令，创建一个新的aftman.toml文件，这是后续管理工具的重要配置文件。\n\u003cstrong\u003e（这部分也以使用npm进行，不过wally好像在npm中没有包，可以自己放入环境变量中，rojo可以直接使用npm进行安装，比aftman方便些）\u003c/strong\u003e\u003c/p\u003e\n\u003ch2 id=\"添加工具\"\u003e添加工具\u003c/h2\u003e\n\u003ch3 id=\"添加-rojo\"\u003e添加 rojo\u003c/h3\u003e\n\u003cp\u003e添加工具时，以 rojo 为例（==我这里使用的是当前的最新版，现在最新可能不是这个版本==）你可以使用以下命令：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 添加最新版本的rojo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaftman add rojo-rbx/rojo\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 添加指定版本7.5.1的rojo\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaftman add rojo-rbx/rojo@7.5.1\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 以不同的二进制名称添加工具，例如添加ripgrep并命名为rg\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaftman add BurntSushi/ripgrep rg\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"添加-wally\"\u003e添加 wally\u003c/h3\u003e\n\u003cp\u003e使用 aftman 添加 wally 的流程如下：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 初始化aftman配置\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaftman init\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 添加wally工具\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaftman add UpliftGames/wally\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 安装已添加的工具\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003eaftman install\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch4 id=\"配置-wally\"\u003e配置 wally\u003c/h4\u003e\n\u003cp\u003e在 wally 中添加服务器或客户端的包，需要对配置文件进行设置。在相关配置文件中，通过以下内容进行配置：\u003c/p\u003e","title":"Roblox 游戏开发中使用vscode"},{"content":"核心功能实现 区域检测\n使用 PartZone 模块创建检测区域（名为 \u0026ldquo;water\u0026rdquo;）（可以使用ZonePlus来实现） 当玩家进入指定 Part（Sea）时激活游泳状态 玩家离开区域时恢复正常状态 游泳状态控制\n1 2 3 4 5 6 -- 进入水体时 humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false) -- 禁用跳跃 humanoid:ChangeState(Enum.HumanoidStateType.Swimming) -- 强制游泳状态 -- 离开水体时 humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true) -- 恢复跳跃能力 使用LinearVelocity推动人物移动\n创建 LinearVelocity 对象施加推进力 1 2 3 Swim = Instance.new(\u0026#34;LinearVelocity\u0026#34;) Swim.MaxAxesForce = Vector3.new(3000,3000,3000) Swim.Attachment0 = Root.RootAttachment -- 绑定到角色根部件 游泳运动机制\n1 2 3 4 function Swimming() -- 按移动方向和步行速度推进 Swim.VectorVelocity = Humanoid.MoveDirection * Humanoid.WalkSpeed end 关键代码设计 状态管理优化\n动态创建/销毁 LinearVelocity 对象（Swim） 离开水域时通过 Swim:Destroy() 释放资源 每帧运行\n1 2 3 RS.PreRender:Connect(function() if Swim then Swimming() end -- 每帧更新游泳动力 end) 利用 PreRender 事件保证流畅的水中移动 帧率无关的物理更新 玩家的状态设置\n1 humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false) 避免角色在水体中出现异常姿态 使用注意事项 必要依赖\n需提前准备一个工具包，这个包的功能包含能检测玩家进入和离开区域，可以用使用ZonePlus，我这里使用的是我自己写的垃圾模块。 场景中需存在名为 \u0026ldquo;water\u0026rdquo; 的 Part 作为水体区域 角色适配\n自动处理角色加载等待：CharacterAdded:Wait() 兼容动态角色重置情况 物理参数调节\n通过修改 MaxAxesForce 调整游泳灵敏度 可扩展浮力控制（增加 Y 轴向上力） 完整代码 swim.luau\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 local Sea=game.Workspace:WaitForChild(\u0026#34;water\u0026#34;) local PartZone=require(game:GetService(\u0026#34;ReplicatedStorage\u0026#34;).PartZone.PartZoneCore) local StarterGui=game:GetService(\u0026#34;StarterGui\u0026#34;) local RS:RunService=game:GetService(\u0026#34;RunService\u0026#34;) local Character = game.Players.LocalPlayer.Character or game.Players.LocalPlayer.CharacterAdded:Wait() local Humanoid = Character:WaitForChild(\u0026#34;Humanoid\u0026#34;) local Root = Character:WaitForChild(\u0026#34;HumanoidRootPart\u0026#34;) local Swim:LinearVelocity PartZone.CreateZone(\u0026#34;water\u0026#34;,Sea, -- 进入区域触发 function(player:Player) local character = player.Character local humanoid = character:FindFirstChild(\u0026#34;Humanoid\u0026#34;) if not Swim then Swim = Instance.new(\u0026#34;LinearVelocity\u0026#34;) Swim.Parent = Root Swim.ForceLimitMode=Enum.ForceLimitMode.PerAxis Swim.MaxAxesForce=Vector3.new(3000,3000,3000) Swim.Attachment0=Root.RootAttachment end Swim.VectorVelocity=Vector3.new(0,0,0) if humanoid then humanoid:SetStateEnabled(Enum.HumanoidStateType.GettingUp, false) humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false) humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false) humanoid:ChangeState(Enum.HumanoidStateType.Swimming) end end, -- 离开区域触发 function(player:Player) local character = player.Character local humanoid = character:FindFirstChild(\u0026#34;Humanoid\u0026#34;) Swim:Destroy() Swim=nil if humanoid then humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, true) humanoid:SetStateEnabled(Enum.HumanoidStateType.GettingUp, true) humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, true) end end ) local function Swimming() --if Root.Position.Y \u0026lt; Sea.Position.Y then --\tSwim.VectorVelocity=Humanoid.MoveDirection * Humanoid.WalkSpeed --end Swim.VectorVelocity=Humanoid.MoveDirection * Humanoid.WalkSpeed end RS.PreRender:Connect(function(deltaTimeRender: number) if Swim then Swimming() end end) ","permalink":"https://yulan233.github.io/posts/roblox-%E4%BB%BB%E6%84%8F-part-%E5%8C%BA%E5%9F%9F%E6%B8%B8%E6%B3%B3%E5%8A%9F%E8%83%BD/","summary":"\u003ch3 id=\"核心功能实现\"\u003e核心功能实现\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e区域检测\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e使用 \u003ccode\u003ePartZone\u003c/code\u003e 模块创建检测区域（名为 \u0026ldquo;water\u0026rdquo;）（\u003cstrong\u003e可以使用\u003ccode\u003eZonePlus\u003c/code\u003e来实现\u003c/strong\u003e）\u003c/li\u003e\n\u003cli\u003e当玩家进入指定 Part（Sea）时激活游泳状态\u003c/li\u003e\n\u003cli\u003e玩家离开区域时恢复正常状态\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e游泳状态控制\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e6\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e-- 进入水体时\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ehumanoid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eSetStateEnabled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEnum.HumanoidStateType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJumping\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003efalse\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e  \u003cspan class=\"c1\"\u003e-- 禁用跳跃\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ehumanoid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eChangeState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEnum.HumanoidStateType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eSwimming\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e           \u003cspan class=\"c1\"\u003e-- 强制游泳状态\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e-- 离开水体时\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003ehumanoid\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eSetStateEnabled\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eEnum.HumanoidStateType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eJumping\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"kc\"\u003etrue\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e   \u003cspan class=\"c1\"\u003e-- 恢复跳跃能力\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e使用\u003ccode\u003eLinearVelocity\u003c/code\u003e推动人物移动\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e创建 \u003ccode\u003eLinearVelocity\u003c/code\u003e 对象施加推进力\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSwim\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eInstance.new\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;LinearVelocity\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSwim.MaxAxesForce\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eVector3.new\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"mi\"\u003e3000\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"mi\"\u003e3000\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e\u003cspan class=\"mi\"\u003e3000\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eSwim.Attachment0\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eRoot.RootAttachment\u003c/span\u003e           \u003cspan class=\"c1\"\u003e-- 绑定到角色根部件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e游泳运动机制\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e4\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003efunction\u003c/span\u003e \u003cspan class=\"nf\"\u003eSwimming\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 按移动方向和步行速度推进\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eSwim.VectorVelocity\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eHumanoid.MoveDirection\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eHumanoid.WalkSpeed\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch3 id=\"关键代码设计\"\u003e关键代码设计\u003c/h3\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e状态管理优化\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e动态创建/销毁 \u003ccode\u003eLinearVelocity\u003c/code\u003e 对象（\u003ccode\u003eSwim\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e离开水域时通过 \u003ccode\u003eSwim:Destroy()\u003c/code\u003e 释放资源\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e每帧运行\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRS.PreRender\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eConnect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kr\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e  \u003cspan class=\"kr\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eSwim\u003c/span\u003e \u003cspan class=\"kr\"\u003ethen\u003c/span\u003e \u003cspan class=\"n\"\u003eSwimming\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e \u003cspan class=\"kr\"\u003eend\u003c/span\u003e  \u003cspan class=\"c1\"\u003e-- 每帧更新游泳动力\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e利用 \u003ccode\u003ePreRender\u003c/code\u003e 事件保证流畅的水中移动\u003c/li\u003e\n\u003cli\u003e帧率无关的物理更新\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e玩家的状态设置\u003c/strong\u003e\u003c/p\u003e","title":"Roblox 任意 Part 区域游泳功能"},{"content":"在 Roblox 开发中，我们经常需要获取一个不受玩家本地时区影响的标准时间，即 UTC（协调世界时）。虽然在服务器上可以直接使用 os.time()，因为它本身就在一个受控的环境中运行，但在客户端，玩家的设备时区各不相同，直接获取本地时间可能会导致数据不一致。 这里记录一个从客户端获取 UTC 时间的 Lua 脚本函数，并对其原理进行简单说明。\n脚本代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 TimeAbout = {} TimeAbout.GetUTC = function(add:number?) -- 如果传入的参数不是数字，则默认为 0 if type(add) ~= \u0026#34;number\u0026#34; then add = 0 end -- 获取当前本地时间戳 local currentTime = os.time() -- 使用 \u0026#34;!\u0026#34; 标记获取当前时间的 UTC 时间表 local UTC = os.date(\u0026#34;!*t\u0026#34;, currentTime) -- 获取当前时间的本地时间表 local localtime = os.date(\u0026#34;*t\u0026#34;, currentTime) -- 计算本地时间与 UTC 时间的时间差（秒） -- os.difftime 会返回两个时间戳的差值 local diff = os.difftime(os.time(UTC), os.time(localtime)) -- 将本地时间戳加上时差，得到 UTC 时间戳 local utcTime = currentTime + diff -- 返回 UTC 时间戳，并加上可选的小时偏移量 return utcTime + add * 3600 end 实现原理说明 这个函数的核心思想是计算出本地时间与 UTC 时间之间的时差，然后用这个时差来修正本地时间戳。\n获取本地时间戳：os.time() 函数返回一个表示当前本地时间的 Unix 时间戳（自 1970 年 1 月 1 日以来的秒数）。\n获取 UTC 和本地时间表：\nos.date(\u0026quot;!*t\u0026quot;, currentTime)：os.date 函数的第一个参数是格式化字符串。当字符串以 ! 开头时，它会将时间解释为 UTC 时间，而不是本地时间。*t 则表示返回一个包含年、月、日、时等字段的时间表。 os.date(\u0026quot;*t\u0026quot;, currentTime)：不加 ! 则返回本地时间的时间表。 计算时差：\nos.time(UTC) 和 os.time(localtime) 会将这两个时间表分别转换回时间戳。由于 UTC 表是基于 UTC 时间的，而 localtime 表是基于本地时间的，这两个时间戳之间的差值 diff 正好是本地时区与 UTC 的偏移量（以秒为单位）。 例如，如果本地时间是东八区（UTC+8），那么本地时间戳会比 UTC 时间戳大 8 * 3600 = 28800 秒。因此 diff 的值就是 -28800。 修正并返回：\n将原始的本地时间戳 currentTime 加上这个时差 diff，就得到了准确的 UTC 时间戳 utcTime。\nadd 参数是一个可选的数字，代表要增加或减少的小时数。函数会将其转换为秒（add * 3600）并加到最终的 UTC戳上，方便进行时间计算。\n结尾 可能从服务端直接获取时间会更好一些，不过这也是一种不依靠服务端就能获取到utc时间的方法，可能有时候会有需要。\n","permalink":"https://yulan233.github.io/posts/roblox%E5%AE%A2%E6%88%B7%E7%AB%AF%E8%84%9A%E6%9C%AC%E8%8E%B7%E5%8F%96utc%E6%97%B6%E9%97%B4%E7%9A%84%E6%96%B9%E6%B3%95/","summary":"\u003cp\u003e在 Roblox 开发中，我们经常需要获取一个不受玩家本地时区影响的标准时间，即 UTC（协调世界时）。虽然在服务器上可以直接使用 \u003ccode\u003eos.time()\u003c/code\u003e，因为它本身就在一个受控的环境中运行，但在客户端，玩家的设备时区各不相同，直接获取本地时间可能会导致数据不一致。\n这里记录一个从客户端获取 UTC 时间的 Lua 脚本函数，并对其原理进行简单说明。\u003c/p\u003e\n\u003ch3 id=\"脚本代码\"\u003e脚本代码\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e12\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e13\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e14\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e15\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e16\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e17\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e18\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e19\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e20\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e21\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e22\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e23\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e24\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e25\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eTimeAbout\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e{}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eTimeAbout.GetUTC\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"kr\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003enumber\u003c/span\u003e\u003cspan class=\"err\"\u003e?\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 如果传入的参数不是数字，则默认为 0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kr\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003etype\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eadd\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e~=\u003c/span\u003e \u003cspan class=\"s2\"\u003e\u0026#34;number\u0026#34;\u003c/span\u003e \u003cspan class=\"kr\"\u003ethen\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e        \u003cspan class=\"n\"\u003eadd\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kr\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 获取当前本地时间戳\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003elocal\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentTime\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eos.time\u003c/span\u003e\u003cspan class=\"p\"\u003e()\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 使用 \u0026#34;!\u0026#34; 标记获取当前时间的 UTC 时间表\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003elocal\u003c/span\u003e \u003cspan class=\"n\"\u003eUTC\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eos.date\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;!*t\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 获取当前时间的本地时间表\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003elocal\u003c/span\u003e \u003cspan class=\"n\"\u003elocaltime\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eos.date\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;*t\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentTime\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 计算本地时间与 UTC 时间的时间差（秒）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- os.difftime 会返回两个时间戳的差值\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003elocal\u003c/span\u003e \u003cspan class=\"n\"\u003ediff\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003eos.difftime\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eos.time\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eUTC\u003c/span\u003e\u003cspan class=\"p\"\u003e),\u003c/span\u003e \u003cspan class=\"n\"\u003eos.time\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003elocaltime\u003c/span\u003e\u003cspan class=\"p\"\u003e))\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 将本地时间戳加上时差，得到 UTC 时间戳\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kd\"\u003elocal\u003c/span\u003e \u003cspan class=\"n\"\u003eutcTime\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"n\"\u003ecurrentTime\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003ediff\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"c1\"\u003e-- 返回 UTC 时间戳，并加上可选的小时偏移量\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kr\"\u003ereturn\u003c/span\u003e \u003cspan class=\"n\"\u003eutcTime\u003c/span\u003e \u003cspan class=\"o\"\u003e+\u003c/span\u003e \u003cspan class=\"n\"\u003eadd\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"mi\"\u003e3600\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch3 id=\"实现原理说明\"\u003e实现原理说明\u003c/h3\u003e\n\u003cp\u003e这个函数的核心思想是计算出本地时间与 UTC 时间之间的时差，然后用这个时差来修正本地时间戳。\u003c/p\u003e","title":"Roblox客户端脚本获取UTC时间的方法"},{"content":"在 Roblox 开发中，实现 GUI 悬停效果时，MouseEnter 和 MouseLeave 事件有时会表现得不稳定。本文记录了这些传统方法遇到的问题，并介绍一个更可靠的替代方案：使用 GuiObject 的 GuiState 属性。\n传统方法及其局限性 常用的 MouseEnter 和 MouseLeave 事件存在一些已知问题：\n快速移动导致的事件丢失：当鼠标快速划过 GUI 元素时，引擎可能无法正确触发 MouseEnter 或 MouseLeave，导致效果缺失或闪烁。 删除元素不会触发Leave：当你删除绑定了MouseLeave的元素的时候不会触发这个事件，会导致很多问题。 动态 GUI 的处理复杂：在鼠标悬停期间，如果 GUI 对象的位置或大小发生变化，事件监听逻辑容易出错。 另一种尝试是使用 InputBegan 和 InputEnded 事件，并检查 UserInputType 是否为 MouseMovement。 1 2 3 4 5 6 7 8 9 10 11 -- 使用 InputBegan/Ended 的示例（不推荐） Object.InputBegan:Connect(function(Input) if Input.UserInputType == Enum.UserInputType.MouseMovement then print(\u0026#34;Mouse entered\u0026#34;) end end) Object.InputEnded:Connect(function(Input) if Input.UserInputType == Enum.UserInputType.MouseMovement then print(\u0026#34;Mouse left\u0026#34;) end end) 这种方法检测的是“鼠标在对象上开始/停止移动”，而非真正的“进入/离开”。当鼠标静止在对象上然后直接移出时，InputEnded 可能不会被触发。它同样会受到子元素的干扰。\n使用 GuiState 属性 一个更稳定的方案是监听 GuiObject 的 GuiState 属性。GuiState 是一个枚举，它直接反映了 GUI 对象的当前交互状态。其中两个关键状态是：\nEnum.GuiState.Hover：鼠标指针悬停在对象上。 Enum.GuiState.Idle：鼠标指针不在对象上。 通过监听 GuiState 的变化，可以获得一个由引擎底层管理的状态信号，从而避免了上述传统方法的问题。 结合 TweenService 的实现示例 以下代码展示了如何结合 TweenService 和 GuiState 来实现一个带动画和音效的按钮悬停效果。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 -- 获取服务 local TweenService = game:GetService(\u0026#34;TweenService\u0026#34;) -- 定义动画信息 local TWEEN_INFO = TweenInfo.new( 0.1, -- 动画时长 Enum.EasingStyle.Linear, -- 缓动样式 Enum.EasingDirection.InOut -- 缓动方向 ) -- 获取按钮对象（假设此脚本位于按钮内部） local Button = script.Parent local HoverSound = Button:WaitForChild(\u0026#34;Sound\u0026#34;) -- 初始化 Tween 变量 local tween = TweenService:Create(Button, TWEEN_INFO, {}) -- 监听 \u0026#34;GuiState\u0026#34; 属性的变化 Button:GetPropertyChangedSignal(\u0026#34;GuiState\u0026#34;):Connect(function() if Button.GuiState == Enum.GuiState.Hover then -- 鼠标悬停时 tween:Cancel() -- 取消当前动画 tween = TweenService:Create(Button, TWEEN_INFO, { Size = UDim2.new(0, 250, 0, 63) -- 悬停尺寸 }) tween:Play() HoverSound:Play() elseif Button.GuiState == Enum.GuiState.Idle then -- 鼠标离开时 tween:Cancel() -- 取消当前动画 tween = TweenService:Create(Button, TWEEN_INFO, { Size = UDim2.new(0, 210, 0, 63) -- 原始尺寸 }) tween:Play() end end) 代码要点说明 GetPropertyChangedSignal(\u0026quot;GuiState\u0026quot;)：此方法创建一个事件，仅在 GuiState 属性值改变时触发，比逐帧检查更高效。 状态判断：在回调函数中，直接检查 Button.GuiState 的值来执行相应逻辑。Hover 对应鼠标进入，Idle 对应鼠标离开。 tween:Cancel()：在创建新动画前取消旧动画，可以防止用户快速移入移出时，多个动画排队执行导致的视觉延迟或抖动。 动态创建 Tween：每次状态改变时都重新创建 Tween 对象，并设定新的目标属性，使代码逻辑清晰。 总结 对于 Roblox GUI 开发，使用 GuiState 属性来处理悬停效果是比 MouseEnter 和 MouseLeave 更可靠的选择。它在处理子元素、快速移动等场景时表现更稳定，同时代码逻辑也更直接。在新的项目中，建议优先考虑使用 GuiState。\n","permalink":"https://yulan233.github.io/posts/%E5%85%B3%E4%BA%8E-roblox-gui-%E6%82%AC%E5%81%9C%E6%95%88%E6%9E%9C%E7%9A%84%E5%AE%9E%E7%8E%B0/","summary":"\u003cp\u003e在 Roblox 开发中，实现 GUI 悬停效果时，\u003ccode\u003eMouseEnter\u003c/code\u003e 和 \u003ccode\u003eMouseLeave\u003c/code\u003e 事件有时会表现得不稳定。本文记录了这些传统方法遇到的问题，并介绍一个更可靠的替代方案：使用 \u003ccode\u003eGuiObject\u003c/code\u003e 的 \u003ccode\u003eGuiState\u003c/code\u003e 属性。\u003c/p\u003e\n\u003ch4 id=\"传统方法及其局限性\"\u003e传统方法及其局限性\u003c/h4\u003e\n\u003cp\u003e常用的 \u003ccode\u003eMouseEnter\u003c/code\u003e 和 \u003ccode\u003eMouseLeave\u003c/code\u003e 事件存在一些已知问题：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e快速移动导致的事件丢失\u003c/strong\u003e：当鼠标快速划过 GUI 元素时，引擎可能无法正确触发 \u003ccode\u003eMouseEnter\u003c/code\u003e 或 \u003ccode\u003eMouseLeave\u003c/code\u003e，导致效果缺失或闪烁。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e删除元素不会触发Leave\u003c/strong\u003e：当你删除绑定了\u003ccode\u003eMouseLeave\u003c/code\u003e的元素的时候不会触发这个事件，会导致很多问题。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e动态 GUI 的处理复杂\u003c/strong\u003e：在鼠标悬停期间，如果 GUI 对象的位置或大小发生变化，事件监听逻辑容易出错。\n另一种尝试是使用 \u003ccode\u003eInputBegan\u003c/code\u003e 和 \u003ccode\u003eInputEnded\u003c/code\u003e 事件，并检查 \u003ccode\u003eUserInputType\u003c/code\u003e 是否为 \u003ccode\u003eMouseMovement\u003c/code\u003e。\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv class=\"chroma\"\u003e\n\u003ctable class=\"lntable\"\u003e\u003ctr\u003e\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode\u003e\u003cspan class=\"lnt\"\u003e 1\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 2\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 3\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 4\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 5\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 6\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 7\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 8\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e 9\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e10\n\u003c/span\u003e\u003cspan class=\"lnt\"\u003e11\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd class=\"lntd\"\u003e\n\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-lua\" data-lang=\"lua\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e-- 使用 InputBegan/Ended 的示例（不推荐）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eObject.InputBegan\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eConnect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kr\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eInput\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kr\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eInput.UserInputType\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eEnum.UserInputType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMouseMovement\u003c/span\u003e \u003cspan class=\"kr\"\u003ethen\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"n\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Mouse entered\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eObject.InputEnded\u003c/span\u003e\u003cspan class=\"p\"\u003e:\u003c/span\u003e\u003cspan class=\"n\"\u003eConnect\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kr\"\u003efunction\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003eInput\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kr\"\u003eif\u003c/span\u003e \u003cspan class=\"n\"\u003eInput.UserInputType\u003c/span\u003e \u003cspan class=\"o\"\u003e==\u003c/span\u003e \u003cspan class=\"n\"\u003eEnum.UserInputType\u003c/span\u003e\u003cspan class=\"p\"\u003e.\u003c/span\u003e\u003cspan class=\"n\"\u003eMouseMovement\u003c/span\u003e \u003cspan class=\"kr\"\u003ethen\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\t\u003cspan class=\"n\"\u003eprint\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"s2\"\u003e\u0026#34;Mouse left\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"kr\"\u003eend\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e这种方法检测的是“鼠标在对象上开始/停止移动”，而非真正的“进入/离开”。当鼠标静止在对象上然后直接移出时，\u003ccode\u003eInputEnded\u003c/code\u003e 可能不会被触发。它同样会受到子元素的干扰。\u003c/p\u003e","title":"关于 Roblox GUI 悬停效果的实现"}]