Valheim
Install with App

Details

Latest version
1.3.0
Last Updated
First Uploaded
Downloads
305
Likes
0
Size
56KB
Dependants
ADDatHost Valheim hosting
€1

EasyTeleport

大地图上的传送按钮。 打开地图(M),画面左上角出现两个按钮 —— 外观默认与游戏原生控件同化(运行时继承原版面板底图),不会显得像外挂的贴纸。

按钮 做什么
传送回床 传送到你记录过的床(在床上睡过觉设下的出生点)。
任意传送 按一下 → 按钮变绿进入待选 → 再点地图上任意位置,就传送到那里。
再按一次按钮可取消。

挪位置:按住 Ctrl 用左键拖拽按钮组,可以放到画面任意位置,松手自动保存。 按住 Ctrl 时按钮文字会变成淡黄色,表示"现在可以拖"。

传送落地是安全的:陆地自动落到地面上方、点水面会浮在水上游泳、永远不会摔死。 传送在任何状态下都能用 —— 游泳、奔跑、跳跃、在船上、在水里都行。

AI 生成声明:本模组由 AI 智能体(WorkBuddy)在用户的明确需求与逐项验收下编写; 需求描述、交互设计与发布决定均由用户主导。


用法

传送回床

  1. 先在床边上按 E 睡一觉(这一步会在角色档案里记下出生点;不睡就点按钮会提示"还没有记录过床")。
  2. M 打开大地图 → 右下角点 「传送回床」 → 黑屏淡入淡出 → 人已经在床边。

任意传送(地图上点哪去哪)

  1. M 打开大地图。
  2. 「任意传送」 —— 按钮变成绿色并显示「取消 · 点地图选点」,同时左上角弹一条提示。
  3. 地图上点任意位置 → 直接传送过去。
    • 点偏了?再点一次地图就行(待选状态会保留到你成功传送或主动取消)。
    • 不想传了?再点一次按钮取消,或者按 M 关掉地图(也会自动取消)。

⚠️ 游戏对传送有 2 秒冷却(原版传送门同款)。刚传送完立刻再传会被拒绝, 屏幕上会提示"传送被游戏拒绝 —— 通常是因为刚传送过"。此时待选状态不会丢,等一两秒直接再点地图即可。


落点是怎么保证安全的

这一块没有自己写地形算法 —— 而是利用了原版 Player.UpdateTeleport本就有的一套落地校正流程, 再喂给它一个"在什么地形上都成立"的高度值。所有结论都来自对 assembly_valheim.dll 的反汇编。

原版的流程(Player.UpdateTeleportdistantTeleport = true 时)

①  player.position = 我们给的 (x, Y, z)          ← 这里 Y 是我们唯一能控制的
②  等 ZNetScene.IsAreaReady(target)              ← 远处传送就是在这里等区域加载
③  if (ZoneSystem.FindFloor(target, out floor))  ← Raycast(target + up×1, down, 1000 m)
        return;                                  ← ★ 命中就结束,玩家高度 = 我们给的 Y(不再校正)
④  else(没命中)
        player.y = ZoneSystem.GetSolidHeight(target) + 0.5
                                                 ← Raycast(target + up×1000, down, 2000 m)

同时,在搬运玩家的那一刻原版会执行 Character.m_maxAirAltitude = transform.position.y; 摔落伤害 = 当前高度 − 历史最高滞空高度,所以传送落地时该差值恒为 0 ⇒ 不会摔伤

我们取的 Y = ZoneSystem.c_WaterLevel

ZoneSystem.c_WaterLevel 是一个 public const,从 DLL 的 Constant 表里读出来是 30.0f(Valheim 海平面)。 取这个值,两条分支在地形上的表现是:

地形 ③ FindFloor 最终落点
陆地(地面高于水面) 射线从水面上方往下打够不到地面 → 未命中 走 ④ → 精确落到地面 +0.5 米
海洋 / 湖泊(水底低于水面) 射线命中水底 → 命中 高度保持水面 → 玩家浮在水面游泳

离线场景矩阵验证(scripts/verify_landing.py,12 个场景)全部通过:

高山(Deep North 峰)   地面 420.0 → GetSolidHeight 校正 → 落点 420.5  OK 落在地面 +0.5 米
山地                    地面 180.0 → GetSolidHeight 校正 → 落点 180.5  OK 落在地面 +0.5 米
平原 / 草原             地面  50.0 → GetSolidHeight 校正 → 落点  50.5  OK 落在地面 +0.5 米
略高于水面的海岸         地面  32.5 → GetSolidHeight 校正 → 落点  33.0  OK 落在地面 +0.5 米
刚好在水面              地面  30.0 → FindFloor 命中      → 落点  30.0  OK
浅滩(水下 5 米)        地面  25.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
沼泽低地(水下 12 米)    地面  18.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
浅湖底(水下 25 米)     地面   5.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
深海海床                地面 -55.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
最深海床                地面 -95.0 → FindFloor 命中      → 落点  30.0  OK 落在水面(可游泳)
世界最高峰(极端假设)    地面 900.0 → GetSolidHeight 校正 → 落点 900.5  OK 落在地面 +0.5 米
超出校正范围的极端高      地面1100.0 → 两条都失败          → 落点1030.0  FAIL(Valheim 无此地形)

⚠️ 这也解释了为什么 UseDistantTeleport 必须保持 true: 只有远距离传送模式才会在 FindFloor 未命中时执行第 ④ 步校正。 把它关掉,跨半张地图传送就可能"人到了但地面还没来",或者停在未校正的高度上。


位置与外观

位置:按住 Ctrl 拖拽就行

按住 Ctrl + 左键拖动按钮组 → 放到画面任意位置 → 松手自动保存到 easyteleport.client.only.cfgLeftAnchor / TopAnchor,不必手动改配置。

  • 按住 Ctrl 时按钮文字变淡黄,表示"现在可以拖"。
  • 拖拽跟手:从按钮组的哪一点起拖,那一点就一直跟着鼠标(不会跳到鼠标中心)。
  • 拖到屏幕外面会被夹在边界内,不会把按钮甩丢。
  • 拖拽期间、以及松手后 0.25 秒内按钮功能被抑制 —— 不会"想挪位置却把自己传走了"。
  • 不想要这个功能就把 AllowDrag 设为 false

坐标用比例锚点(画面宽的 LeftAnchor、高的 TopAnchor),默认 0.084 / 0.392 = 1920×1080 下大约 (162, 423)。用比例而不是像素边距,是为了换分辨率时位置不会跑偏。

外观:继承游戏原生 UI

UseGameStyle = true(默认)时,模组会在运行时从大地图已有的控件里翻一个"面板/按钮底图" sprite 过来复用:

优先级 来源
名字里带 button / panel / background / _bg / frame 的 Image
有九宫格边界(Sprite.border != 0)的 Image —— 这种才是可拉伸的面板底图
任意带图的 Image
都没有 → 退回纯色半透明深灰

拿到之后就按九宫格(Image.Type.Sliced)铺满按钮,所以圆角、渐变、描边都跟原版控件一致。 文字用米白色 + 1 像素黑描边,和原版 UI 的文字处理方式一致。

⚠️ 开着 Verbose 可以在日志里看到底图具体取自哪个控件(按钮底图取自 … sprite=… 九宫格=True)。 万一某次更新后取到的图不合适(比如拿到一个小图标铺开变形),把 UseGameStyle 改成 false 即可退回干净的纯色底 —— 这时按钮的长相由 ArmedColor 等配置控制。

为什么中文字体不能跟着游戏走

游戏内置字体 AveriaSansLibre 不含 CJK 字形,用它画中文会是一排方块。 所以字体默认走系统字体(微软雅黑),并从系统字体清单里按名字找。 观感上和原版 UI 已经很接近(都是无衬线体),但确实不是同一个字体 —— 这是中文环境下的必然取舍。


配置

BepInEx/config/easyteleport.client.only.cfg

分区 默认 说明
1 - General Enabled true 总开关
2 - Buttons BackToBedText 传送回床 按钮文字
AnywhereText 任意传送 按钮平时显示的文字
ArmedText 取消 · 点地图选点 待选状态显示的文字
ArmedColor 深绿 待选状态下按钮的绿色调(UseGameStyle = false 时作为底色)
UseGameStyle true 按钮外观继承游戏原生 UI 底图(见下节)
AllowDrag true 允许按住 Ctrl 拖拽调整位置
LeftAnchor 0.084 按钮组左上角的横向比例位置(0=最左,1=最右)· 拖拽会自动写入
TopAnchor 0.392 按钮组左上角的纵向比例位置(0=最上,1=最下)· 拖拽会自动写入
ButtonWidth / ButtonHeight 190 / 40 按钮尺寸(像素)
Spacing 6 两按钮间距
FontSize 16 字号
FontName Microsoft YaHei UI 字体(见下)
3 - Teleport CloseMapOnTeleport true 传送后自动关掉大地图(回到小地图)
UseDistantTeleport true 远距离传送 —— 落地安全的前提,别关
ShowMessages true 屏幕左上角提示
4 - Debug Verbose false 详细日志

关于字体:游戏内置字体(AveriaSansLibre不含中文字形,直接画中文会变方块, 所以默认走系统字体(微软雅黑)。查找顺序: 配置里的名字 → 一组常见中文字体 → 游戏内置字体 → 已加载的任意字体。 想让按钮跟游戏 UI 完全统一(但只能显示英文),把 FontName 改成 AveriaSansLibre, 同时把按钮文字改成英文即可。

⚠️ 改完配置必须重启游戏(BepInEx 只在插件 Awake 里读一次)。 装了 BepInEx.ConfigurationManager 的话可以按 F1 直接改,那种是即时生效的。


兼容性

  • 纯客户端:无 Jotunn、无 ServerSync、无 NetworkCompatibilityAttribute(= NotEnforced)。
  • 联机:主机和其他玩家都不需要装;没装的人完全不受影响。 本模组只调用游戏自己的接口(Player.TeleportTo),不改任何主机权威数据。
  • 与地图类模组(如 BetterMap)不冲突 —— 按钮挂在游戏自己的大地图根节点下, 只占屏幕右下角一块;位置撞了改 MarginBottom / MarginRight 即可。
  • 传送门附近也可以用,本模组与本机已装的 QuickTeleport 无交互。

实现要点(全部来自 IL 实证,2026-09-18)

床的坐标从哪来

PlayerProfile.HaveCustomSpawnPoint()   → bool      (public)
PlayerProfile.GetCustomSpawnPoint()    → Vector3   (public)

两者的 IL 都只有三行:ZNet.instance.GetWorldUID()GetWorldData(uid) → 读 WorldPlayerData.m_haveCustomSpawnPoint / m_spawnPoint。 ⇒ 按世界 UID 分开存,换世界不会串坐标。 原版 Minimap.UpdateProfilePins(地图上那个"床"图标)用的也是这两个方法。

⚠️ 没有 Player.m_spawnPoint 这种字段(Player 里跟 spawn 有关的只有 m_spawnEffects)。

地图点击 → 世界坐标

原版 Minimap.OnMapLeftClick(public)里的两步:

ZInput.pointerPosition  →  Vector3(屏幕坐标)
Minimap.ScreenToWorldPoint(屏幕坐标)  →  Vector3(世界坐标,Y 恒为 0,只有 XZ 有效)

ScreenToWorldPointprivate —— 本模组用反射调用它(和原版走完全相同的换算: RectTransformUtility.ScreenPointToLocalPointInRectangleRect.PointToNormalized → 按 RawImage.uvRect 映射 → MapPointToWorld)。

ZInput 定义在 assembly_utils.dll 里(不是 assembly_valheim), 用 AccessTools.TypeByName("ZInput") 反射取 pointerPosition,拿不到时回退 Input.mousePosition

怎么"吞掉"那次点击

Minimap.OnMapLeftClick 是 public,直接挂 Prefix:

[HarmonyPatch(typeof(Minimap), "OnMapLeftClick")]
internal static class MinimapLeftClickPatch
{
    [HarmonyPrefix]
    private static bool Prefix() => MapTeleportButtons.HandleMapLeftClick();
}

返回 false 时原版不会继续走"选中 / 取消选中点到的图钉"那段逻辑。

顺带一提:原版放图钉是中键Minimap.OnMapMiddleClick),所以待选状态下按中键不受影响。

传送走哪条路

Player.TeleportTo(Vector3 pos, Quaternion rot, bool distantTeleport)   (public,返回 bool)

IL 分两支:

if (!m_nview.IsOwner()) {
    m_nview.InvokeRPC("RPC_TeleportTo", pos, rot, distantTeleport);
    return false;                      // ← RPC 已发出去了,false 不代表失败
}
if (IsTeleporting())          return false;
if (m_teleportCooldown < 2f)  return false;      // ← 2 秒冷却在这里
m_teleporting = true;
m_teleportFromPos   = transform.position;
m_teleportTargetPos = pos;
return true;

⇒ 本模组的返回值处理:自己是主人时 true = 真的开始传送了; 不是主人时忽略返回值。玩家自己的角色对象正常就是自己拥有的,这条只是兜底。

按钮挂在哪

Minimap.m_largeRoot : GameObject   (public)
Minimap.IsOpen() 的 IL = s_instance != null && (m_largeRoot.activeSelf || m_hiddenFrames <= 2)

挂在 m_largeRoot 下的好处:地图关掉 → 根节点失活 → 按钮自动隐藏, 不用自己监听地图开关。

⚠️ 三个已经踩过的坑:

  1. MapMode Minimap 的嵌套类型(和 PinData / PinType 一样), 写裸的 MapMode 会 CS0103「当前上下文中不存在名称 MapMode」—— 看着像"类型不存在",其实是嵌套类型没加外部类名。
  2. 不用 IMGUI 画按钮:大地图是全屏 uGUI Canvas,OnGUI 画的东西会被整个盖住 (看不见也点不到)。所以走原生 uGUI。
  3. Minimap.m_nameInput 的类型 GuiInputField 继承 TMP_InputField(TextMeshPro), 在 gui_framework.dll 里,想从它拿字体会连锁引 gui_framework + Unity.TextMeshPro, 而且它没有 .textComponent(那是 uGUI InputField 的)—— 不值得,改用系统字体。

为什么按钮不会误触发地图操作

原版全程序集里 EventSystem.IsPointerOverGameObject 只有 FejdStartup.UpdateCharacterRotation 一个调用者 —— 地图自己不做"鼠标是否落在 UI 上"的判断。 所以防误触靠的是层级隔离:地图的可点击区域(UIInputHandler)挂在 m_mapLarge 上, 按钮挂在它的兄弟节点 m_largeRoot 下,点击传不进地图的输入处理。

Ctrl 拖拽是怎么实现的(5 个必须处理的点)

  1. 拖拽事件不带修饰键。Unity 的 IDragHandler 只告诉你"在拖",不告诉你按没按 Ctrl ⇒ "按住 Ctrl 才允许拖"必须在回调里自己 Input.GetKey(KeyCode.LeftControl) 判断。

  2. 面板得先成为射线目标。面板本身是个空 GameObject,收不到任何事件 ⇒ 给它铺一张全透明 Imagecolor = (0,0,0,0)raycastTarget = true)。 uGUI 的 Graphic.Raycast 默认不看 alpha(除非设了 alphaHitTestMinimumThreshold), 所以 alpha=0 照样能被命中 —— 这就是"透明挡板"的惯用做法。 因为 uGUI 射线按深度排序、子节点优先,按钮自己的点击不受影响。

  3. 事件会沿父级链冒泡。uGUI 执行拖拽事件时用的是 ExecuteEvents.GetEventHandler<IDragHandler>(target),它会从命中对象往上找第一个 实现了该接口的对象 ⇒ 在按钮上按下也能拖,事件最终落到面板的 EventTrigger 上, 不用给每个按钮单独挂。

  4. 必须抑制 Button 的 onClick。uGUI 的 Button 走的是 OnPointerClick —— 只要按下和抬起都落在按钮上就触发,中间拖了多远它不管 ⇒ 拖完松手会顺带把按钮点一次。 这里用"拖拽中 + 松手后 0.25 秒"的时间窗口来抑制,而不是一个布尔标志, 因为 EventSystem 与插件 Update 的先后顺序在 Unity 里没有保证, 靠标志会踩到"标志已清、onClick 还没跑"的时序问题。

  5. 拖拽过程中绝不写配置。BepInEx 的 ConfigEntry.Value 赋值会立刻存盘, 拖拽时每帧写一次会把 cfg 写爆 ⇒ 拖拽中只改内存里的 liveAnchorX/YRectTransform, 到 OnEndDrag 才写回一次。

坐标换算(屏幕坐标 → 锚点比例):

local = ScreenPointToLocalPointInRectangle(parentRt, mousePos, cam, out local)
ratio = (local - parentRect.rect.min) / parentRect.rect.size

rect.min 是矩形左下角相对 pivot 的位置,减掉它就把"相对 pivot 的坐标"转成"相对左下角的坐标", 再除以尺寸就是 0~1 的比例。 ⚠️ ScreenPointToLocalPointInRectangle 的 camera 参数:Screen Space - Overlay 的 Canvas 要传 null, Camera 模式才传 worldCamera —— 传错会得到全零或离谱的坐标。 ⚠️ Canvas / RenderMode / RectTransformUtility 三个类型都在 UnityEngine.UIModule.dll 里(不是 UnityEngine.UI.dll),csproj 少引会报 CS1069/CS0103。

跟手性的充要条件:按下时记录"鼠标相对面板左上角的偏移" grab, 拖动时令 锚点 = 鼠标比例 - grab ⇒ 该偏移在整个拖拽过程中恒定,面板就不会往鼠标中心跳。 离线验证见 scripts/verify_drag.py(7 个场景:跟手性、上下界 clamp、100 步无漂移、配置往返)。


已知限制

  • 只能在已探索的地图区域点选 —— 大地图本身只显示探索过的范围,点到未探索区域等于盲传 (游戏仍会正确落地,但你不知道那是哪)。
  • 传送到床需要角色在床上睡过至少一次
  • 床的落点是上次睡觉时记录的那个位置;之后把床拆了 / 挪了,原版记录不会自动更新(要重新睡一觉)。
  • 待选状态在关闭地图时会自动取消;退出游戏时会连同按钮状态一起重置(不落盘)。
  • 极少数带反作弊的多人服务器可能拒绝客户端自行改位置;自用 / 自建服无此问题。

安装

r2modman「Install from file」选 EasyTeleport-1.1.0.zip, 或把 EasyTeleport.dll 丢进 BepInEx/plugins/

前置:BepInExPack_Valheim 5.4.2350

版本

  • 1.3.0(2026-09-18):按钮支持按住 Ctrl 拖拽调整位置,松手自动写回配置(新增 AllowDrag)。
  • 1.2.0(2026-09-18):按钮从左下角移到画面左上角(比例锚点定位), 外观改为继承游戏原生 UI 底图UseGameStyle),尺寸与字号按原版控件收窄。
  • 1.1.0(2026-09-18):把「返回出发地」换成**「任意传送」**(地图上点哪去哪), 并接入原版的落地校正流程(水面高度 + FindFloor / GetSolidHeight),永不埋地、不摔死。
  • 1.0.0(2026-09-18):首个版本(回床 + 返回出发地)。
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro
DatHost — Valheim server hosting, first month for one euro