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] Vanilla-styled world-map buttons: teleport back to your bed, or click anywhere on the map to teleport there. Hold Ctrl to drag the buttons anywhere; position is saved. Safe landing: no burying, no fall damage, water is fine. Client-only.
By s6652289
| Last updated | 4 days ago |
| Total downloads | 308 |
| Total rating | 0 |
| Categories | Mods AI Generated |
| Dependency string | s6652289-EasyTeleport-1.3.0 |
| Dependants | 0 other packages depend on this package |
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),画面左上角出现两个按钮 —— 外观默认与游戏原生控件同化(运行时继承原版面板底图),不会显得像外挂的贴纸。
| 按钮 | 做什么 |
|---|---|
| 传送回床 | 传送到你记录过的床(在床上睡过觉设下的出生点)。 |
| 任意传送 | 按一下 → 按钮变绿进入待选 → 再点地图上任意位置,就传送到那里。 再按一次按钮可取消。 |
挪位置:按住
Ctrl用左键拖拽按钮组,可以放到画面任意位置,松手自动保存。 按住 Ctrl 时按钮文字会变成淡黄色,表示"现在可以拖"。
传送落地是安全的:陆地自动落到地面上方、点水面会浮在水上游泳、永远不会摔死。 传送在任何状态下都能用 —— 游泳、奔跑、跳跃、在船上、在水里都行。
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 未命中时执行第 ④ 步校正。
把它关掉,跨半张地图传送就可能"人到了但地面还没来",或者停在未校正的高度上。
位置与外观
位置:按住 Ctrl 拖拽就行
按住 Ctrl + 左键拖动按钮组 → 放到画面任意位置 → 松手自动保存到
easyteleport.client.only.cfg 的 LeftAnchor / 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 有效)
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 下,点击传不进地图的输入处理。
Ctrl 拖拽是怎么实现的(5 个必须处理的点)
-
拖拽事件不带修饰键。Unity 的
IDragHandler只告诉你"在拖",不告诉你按没按 Ctrl ⇒ "按住 Ctrl 才允许拖"必须在回调里自己Input.GetKey(KeyCode.LeftControl)判断。 -
面板得先成为射线目标。面板本身是个空 GameObject,收不到任何事件 ⇒ 给它铺一张全透明 Image(
color = (0,0,0,0)、raycastTarget = true)。 uGUI 的Graphic.Raycast默认不看 alpha(除非设了alphaHitTestMinimumThreshold), 所以 alpha=0 照样能被命中 —— 这就是"透明挡板"的惯用做法。 因为 uGUI 射线按深度排序、子节点优先,按钮自己的点击不受影响。 -
事件会沿父级链冒泡。uGUI 执行拖拽事件时用的是
ExecuteEvents.GetEventHandler<IDragHandler>(target),它会从命中对象往上找第一个 实现了该接口的对象 ⇒ 在按钮上按下也能拖,事件最终落到面板的EventTrigger上, 不用给每个按钮单独挂。 -
必须抑制 Button 的 onClick。uGUI 的
Button走的是OnPointerClick—— 只要按下和抬起都落在按钮上就触发,中间拖了多远它不管 ⇒ 拖完松手会顺带把按钮点一次。 这里用"拖拽中 + 松手后 0.25 秒"的时间窗口来抑制,而不是一个布尔标志, 因为EventSystem与插件Update的先后顺序在 Unity 里没有保证, 靠标志会踩到"标志已清、onClick 还没跑"的时序问题。 -
拖拽过程中绝不写配置。BepInEx 的
ConfigEntry.Value赋值会立刻存盘, 拖拽时每帧写一次会把 cfg 写爆 ⇒ 拖拽中只改内存里的liveAnchorX/Y和RectTransform, 到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):首个版本(回床 + 返回出发地)。