Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
EasyTeleport
[AI-generated] Top-left world-map buttons styled after the vanilla UI: teleport back to your recorded bed, or click anywhere on the map to teleport there. Landing uses the vanilla flow - never buried, no fall damage, water is fine. Client-only.
By s6652289
| Date uploaded | 2 weeks ago |
| Version | 1.2.0 |
| Download link | s6652289-EasyTeleport-1.2.0.zip |
| Downloads | 17 |
| Dependency string | s6652289-EasyTeleport-1.2.0 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2350README
EasyTeleport
大地图上的传送按钮。 打开地图(M),画面左上角出现两个按钮 —— 外观默认与游戏原生控件同化(运行时继承原版面板底图),不会显得像外挂的贴纸。
| 按钮 | 做什么 |
|---|---|
| 传送回床 | 传送到你记录过的床(在床上睡过觉设下的出生点)。 |
| 任意传送 | 按一下 → 按钮变绿进入待选 → 再点地图上任意位置,就传送到那里。 再按一次按钮可取消。 |
传送落地是安全的:陆地自动落到地面上方、点水面会浮在水上游泳、永远不会摔死。 传送在任何状态下都能用 —— 游泳、奔跑、跳跃、在船上、在水里都行。
AI 生成声明:本模组由 AI 智能体(WorkBuddy)在用户的明确需求与逐项验收下编写; 需求描述、交互设计与发布决定均由用户主导。
用法
传送回床
- 先在床边上按 E 睡一觉(这一步会在角色档案里记下出生点;不睡就点按钮会提示"还没有记录过床")。
- 按
M打开大地图 → 右下角点 「传送回床」 → 黑屏淡入淡出 → 人已经在床边。
任意传送(地图上点哪去哪)
- 按
M打开大地图。 - 点 「任意传送」 —— 按钮变成绿色并显示「取消 · 点地图选点」,同时左上角弹一条提示。
- 在地图上点任意位置 → 直接传送过去。
- 点偏了?再点一次地图就行(待选状态会保留到你成功传送或主动取消)。
- 不想传了?再点一次按钮取消,或者按
M关掉地图(也会自动取消)。
⚠️ 游戏对传送有 2 秒冷却(原版传送门同款)。刚传送完立刻再传会被拒绝, 屏幕上会提示"传送被游戏拒绝 —— 通常是因为刚传送过"。此时待选状态不会丢,等一两秒直接再点地图即可。
落点是怎么保证安全的
这一块没有自己写地形算法 —— 而是利用了原版 Player.UpdateTeleport 里本就有的一套落地校正流程,
再喂给它一个"在什么地形上都成立"的高度值。所有结论都来自对 assembly_valheim.dll 的反汇编。
原版的流程(Player.UpdateTeleport,distantTeleport = 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 未命中时执行第 ④ 步校正。
把它关掉,跨半张地图传送就可能"人到了但地面还没来",或者停在未校正的高度上。
位置与外观
位置:画面左上角,用比例锚点
按钮组的左上角钉在画面的 (LeftAnchor, TopAnchor) 处,默认 0.084 / 0.392,
也就是 1920×1080 下大约第 (162, 423) 像素 —— 位于地图上方那片空白区域,
不会压到地图本身的内容、图例文字、左边和右边的两列图钉图标。
用比例而不是像素边距,是为了换分辨率时位置不会跑偏(像素边距在 4K 下会显得贴边)。
嫌位置不合适就改这两个值:TopAnchor 越小越靠上,LeftAnchor 越小越靠左。
外观:继承游戏原生 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 底图(见下节) | |
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 有效)
ScreenToWorldPoint 是 private —— 本模组用反射调用它(和原版走完全相同的换算:
RectTransformUtility.ScreenPointToLocalPointInRectangle → Rect.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 下的好处:地图关掉 → 根节点失活 → 按钮自动隐藏,
不用自己监听地图开关。
⚠️ 三个已经踩过的坑:
MapMode是Minimap的嵌套类型(和PinData/PinType一样), 写裸的MapMode会 CS0103「当前上下文中不存在名称 MapMode」—— 看着像"类型不存在",其实是嵌套类型没加外部类名。- 不用 IMGUI 画按钮:大地图是全屏 uGUI Canvas,
OnGUI画的东西会被整个盖住 (看不见也点不到)。所以走原生 uGUI。 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 下,点击传不进地图的输入处理。
已知限制
- 只能在已探索的地图区域点选 —— 大地图本身只显示探索过的范围,点到未探索区域等于盲传 (游戏仍会正确落地,但你不知道那是哪)。
- 传送到床需要角色在床上睡过至少一次。
- 床的落点是上次睡觉时记录的那个位置;之后把床拆了 / 挪了,原版记录不会自动更新(要重新睡一觉)。
- 待选状态在关闭地图时会自动取消;退出游戏时会连同按钮状态一起重置(不落盘)。
- 极少数带反作弊的多人服务器可能拒绝客户端自行改位置;自用 / 自建服无此问题。
安装
r2modman「Install from file」选 EasyTeleport-1.1.0.zip,
或把 EasyTeleport.dll 丢进 BepInEx/plugins/。
前置:BepInExPack_Valheim 5.4.2350。
版本
- 1.2.0(2026-09-18):按钮从左下角移到画面左上角(比例锚点定位,可配置),
外观改为继承游戏原生 UI 底图(
UseGameStyle),尺寸与字号按原版控件收窄。 - 1.1.0(2026-09-18):把「返回出发地」换成**「任意传送」**(地图上点哪去哪),
并接入原版的落地校正流程(水面高度 +
FindFloor/GetSolidHeight),永不埋地、不摔死。 - 1.0.0(2026-09-18):首个版本(回床 + 返回出发地)。
CHANGELOG
Changelog
1.3.0 — 2026-09-18
按钮可以拖了。
- 按住
Ctrl+ 左键拖拽按钮组,可以把它整体挪到画面任意位置;松手时自动把新位置写进easyteleport.client.only.cfg的LeftAnchor/TopAnchor,不用手动改配置。 - 按住
Ctrl时按钮文字变淡黄,表示"现在可以拖"。 - 拖拽期间、以及松手后 0.25 秒内抑制按钮功能 —— 不会出现"想挪个位置却把自己传走了"。 (uGUI 的 Button 用的是 OnPointerClick,只要按下和抬起都落在按钮上就会触发, 中间拖了多远它不管,所以必须显式抑制。)
- 新增配置
AllowDrag(默认 true):想锁死位置就关掉它。 - 面板上铺了一张全透明 Image 作为拖拽的射线目标,这样在按钮组的任意位置(含两个按钮之间的缝)
都能起拖;从按钮上起拖时事件会沿父级链冒泡到面板的
EventTrigger,不必给每个按钮单独挂。 - 新增
scripts/verify_drag.py:拖拽定位逻辑离线验证(跟手性 / 边界 clamp / 配置往返,7 个场景)。
1.2.0 — 2026-09-18
按钮搬到左上角 + 外观和游戏同化。
- 位置:右下角 → 画面左上角。用比例锚点定位(
LeftAnchor0.084 /TopAnchor0.392), 默认值是按用户截图上标出的位置量出来的(1920×1080 下约(162, 423)), 不再压住右侧那列图钉图标。 - 外观:新增
UseGameStyle(默认开)—— 运行时从大地图已有控件里挑一个面板/按钮底图 sprite 复用 (优先找名字像按钮的,其次找带九宫格边界的),按Image.Type.Sliced铺满, 圆角/渐变/描边与原版控件一致。找不到或不喜欢可关掉,退回纯色底。 - 文字配色改为米白 + 1 像素黑描边;尺寸收窄到更接近原版控件(宽 190 / 高 40 / 间距 6 / 字号 16)。
- 待选状态下改为给原版底图叠一层绿色调(保留纹理),纯色模式下仍用
ArmedColor。 - 废弃
MarginRight/MarginBottom(旧 cfg 里会留下这两项,无害,可手动删)。
1.1.0 — 2026-09-18
「返回出发地」移除,换成「任意传送」。
- 新增「任意传送」:按一下按钮进入待选(按钮变绿、显示「取消 · 点地图选点」), 再点大地图上任意位置就传送到那里。待选状态下再点一次按钮、或关掉地图,都会取消。
- 移除「返回出发地」(以及配套的"出发地"内存记录)—— 该功能已被"任意点传送"覆盖。
- 落点安全接入原版流程:目标点的 Y 统一取
ZoneSystem.c_WaterLevel(水面高度,Constant 表实测 30.0f)。 这样原版Player.UpdateTeleport的两条分支会自动给出正确落点:- 陆地(
ZoneSystem.FindFloor从水面上方打不到地面)→ 走GetSolidHeight校正 → 落到地面 +0.5 米; - 海洋 / 湖泊(
FindFloor命中水底)→ 高度保持水面 → 玩家浮在水面游泳。 两条路都会设置Character.m_maxAirAltitude = 落点高度,所以摔落伤害恒为 0。
- 陆地(
- 不做任何状态限制:游泳、奔跑、跳跃、坐船、在水里都能传送(只避开"已经正在传送中")。
- 按钮内的 uGUI 层级不变,仍挂在
Minimap.m_largeRoot下;传送后关地图改用原版Minimap.DebugTeleport同款的SetMapMode(MapMode.Small)。 - 新增
scripts/verify_landing.py:按原版 IL 逐条复现的落点策略离线验证(12 个地形场景)。
1.0.0 — 2026-09-18
首个版本。
- 大地图(M)右下角两个按钮:
- 传送回床 —— 传送到
PlayerProfile.GetCustomSpawnPoint()(床边上睡觉记录下的出生点)。 - 返回出发地 —— 先点回床会先记下当前坐标;之后可传回该坐标,记录不被覆盖,可来回。
- 传送回床 —— 传送到
- 传送走原版
Player.TeleportTo(pos, rot, distantTeleport),和传送门完全同一条路径。 - 按钮挂在
Minimap.m_largeRoot下,地图关闭时自动隐藏。 - 中文字体默认走系统字体(微软雅黑),游戏内置字体不含 CJK 字形。
- 纯客户端:无 NetworkCompatibility 声明(NotEnforced)、无 Jotunn、无 ServerSync。