这是安息云峰上最"数学"、也最强大的一个单元。前面你都是用 move 直接走向某个点,但真实的移动往往更复杂——要绕着一个中心镜像转圈、要一边奔向目标一边躲开牦牛、要精确算出该从哪个角度把球踢向骨架。这些都需要一个全新的工具:向量(Vector)。向量能同时表达方向和距离,还能像积木一样相加、缩放、组合。掌握了向量运算,你就拥有了控制"往哪走、走多远"的完整语言。
Vector.subtract 求两点之间的方向与距离向量;用 Vector.add 合成新位置或叠加多个向量;用 Vector.normalize 把向量归一化成长度 1、只保留方向;用 Vector.multiply 缩放向量长度;把"奔向目标"和"远离障碍"两个向量相加,实现避障算法;用 Math.atan2 计算角度;用 findFlag 和 removeFlag 获取与移除旗帜来动态指挥单位。
普通数字(如"5")只有大小,叫标量;向量则同时带有方向和大小,可以想象成一支箭头——箭头指向哪儿是"方向",箭头多长是"距离(幅值 magnitude)"。在游戏里,一个向量常写成 Vector(x, y),其中 x 和 y 是它在两个坐标轴上的分量。同一个向量,既能表示"一个位置坐标",也能表示"从一点到另一点要走的方向和路程"。
一个关键认知:单位的位置 hero.pos 本身就是一个向量——它带着 .x 和 .y 两个分量。所以你可以直接把 hero.pos、partner.pos、ball.pos 当作向量来做加减运算。这也是向量强大的原因:位置、方向、位移都用同一套 Vector 语言表达,可以自由地相互运算。
Vector(40, 34),当我们把它当"位置"用时,它表示地图上坐标 (40, 34) 那个点;当我们把它当"位移/方向"用时,它表示"向右走 40、向上走 34"这段移动。它到底是哪种含义,取决于你怎么用它。
向量减法是本单元最重要的操作。Vector.subtract(A, B) 得到的是一个从 B 指向 A 的向量——它同时告诉你"往哪个方向"和"有多远"。记忆口诀是"目标减自己":想得到"从我这里指向目标"的向量,就写 Vector.subtract(目标.pos, 我.pos)。
# 得到"从英雄指向目标点"的向量 goal = Vector.subtract(goalPoint, hero.pos) # 得到"从牦牛指向英雄"的向量(用于逃离) toHero = Vector.subtract(hero.pos, yak.pos)
subtract(A, B) 和 subtract(B, A) 方向正好相反!想"朝着目标去"用 subtract(目标, 自己);想"远离障碍"用 subtract(自己, 障碍)。顺序写反,就会朝反方向走——这是本单元最致命的错误。
Vector.add 做的是相反的事——把一个位置和一个位移向量相加,得到新的位置。比如"从我当前位置出发,沿着 goal 向量走",落点就是 Vector.add(hero.pos, goal)。加法还能把多个方向向量叠加成一个合成方向,这正是避障算法的核心(后面详述)。
# 从当前位置沿 goal 向量前进,算出落点 moveToPos = Vector.add(hero.pos, goal) hero.move(moveToPos)
subtract 求出方向,加工一下,再 add 回当前位置得到落脚点,最后 move 过去。
有时我们只关心一个向量指向哪儿,不关心它多长。Vector.normalize(v) 会保留 v 的方向,但把它的长度统一变成 1 米(这叫单位向量)。归一化后,我们就能用一个"纯方向",再乘上任意想要的距离,精确控制移动步长——这是"想往这个方向走 X 米"的标准做法。
Vector.multiply(v, X) 把向量 v 的长度乘以 X 倍,方向保持不变。把它和归一化连用,就是控制"移动多远"的黄金组合:先 normalize 归一化成长度 1,再 multiply 乘以想要的米数,就得到一个"朝目标方向、长度恰好 X 米"的向量。
# 黄金三步:subtract 求方向 → normalize 归一化 → multiply 定长度 goal = Vector.subtract(goalPoint, hero.pos) # ① 得到指向目标的向量 goal = Vector.normalize(goal) # ② 压成长度 1,只留方向 goal = Vector.multiply(goal, 10) # ③ 放大到 10 米长
goal = Vector.normalize(goal) 这种"返回新向量再赋值"的写法,也有 toYak.normalize() 这种"直接修改原向量"的链式写法。两者效果相同,链式写法还能连写成 chip.pos.copy().subtract(ballPos).normalize()。用 .copy() 是为了避免改动原始位置。
"滑走"关卡的核心难题是:直线冲向目标会正好撞上牦牛。解决思路极其优雅——把两个向量相加。一个是"奔向目标"的向量 goal,另一个是"远离障碍"的向量(从障碍指向自己,即 subtract(hero.pos, yak.pos))。两者相加得到的合成向量,会让英雄整体朝目标前进、但被障碍"推"得偏开一点,从而绕过去。
# 若牦牛太近,叠加一个"远离牦牛"的向量 if distance < 10: toYak = Vector.subtract(hero.pos, yak.pos) # 从牦牛指向英雄 toYak.normalize() # 只留方向 toYak.multiply(10) # 放大成 10 米的"斥力" goal = Vector.add(goal, toYak) # 叠加到目标向量上 moveToPos = Vector.add(hero.pos, goal) hero.move(moveToPos)
理解避障最直观的方式,是把它想象成物理里的力。目标像磁铁一样对英雄有吸引力(goal 向量),障碍则像同极磁铁一样有排斥力(远离向量)。英雄实际的移动方向,是所有力加起来的合力。障碍越近,斥力应当越强——所以关卡里通常只在"距离小于某个阈值"时才施加斥力,且可以让斥力大小随距离变化。
subtract(自己.pos, 障碍.pos)(从障碍指向自己)。如果写成 subtract(障碍.pos, 自己.pos),就变成"扑向障碍",会直接撞上去。这又一次说明减法顺序至关重要。
有时我们需要的不是向量本身,而是它指向的角度(比如判断球会往哪个方向飞、或让单位面朝某处)。Math.atan2(y, x) 能从一个向量的 y 和 x 分量算出它与水平方向的夹角(弧度)。它比普通的 atan 更聪明——能根据 x、y 的正负正确区分四个象限,给出 -π 到 π 的完整角度。
# 求"从英雄指向敌人"这个方向的角度 diff = Vector.subtract(enemy.pos, hero.pos) angle = Math.atan2(diff.y, diff.x) # 注意:先 y 后 x
Math.atan2 的参数顺序很反直觉——先传 y,再传 x,写成 atan2(diff.y, diff.x)。写反成 atan2(x, y) 会得到错误的角度。这是使用它时最容易踩的坑。
旗帜(flag)是一种动态、临时的指令标记——你在游戏中点击放下一面旗,程序就能读到它并据此行动。hero.findFlag("green") 获取当前放置的绿旗(没有则返回空);hero.removeFlag(flag) 则在用完后把它移除。典型套路是:"如果检测到旗帜,就让最近的单位移向旗帜位置,等它靠近后移除旗帜",这样你就能实时、灵活地指挥球员跑位。
# 检测旗帜 → 派最近单位过去 → 到位后移除旗帜 flag = hero.findFlag("green") if flag: # ...找出离旗帜最近的 closestFriend... hero.command(closestFriend, "move", flag.pos) hero.removeFlag(flag) # 用完立即移除,避免重复触发
removeFlag,那面旗会一直存在,导致每一帧都重复派单位过去、逻辑卡死。"接收指令 → 执行 → 清除指令"是使用旗帜的固定节奏。
这一节聚焦每一关的策略、思路与算法设计,只展示用于说明的关键片段,完整通关程序请你自己动手完成。
本关要让英雄的位置永远和伙伴关于中心 X 点对称——伙伴在中心一侧,英雄就要在正对面的另一侧。这是理解"位置就是向量、减加互为逆运算"的最佳入门关。核心只有两步向量运算。
第一步,用 Vector.subtract(center, partner.pos) 求出"从伙伴指向中心"的向量——它描述了伙伴相对中心的偏移。第二步,把这个偏移从中心再加一次:Vector.add(center, vector),就得到了中心另一侧的对称点。第三步,移动到这个点。因为偏移方向从伙伴→中心,再从中心继续同方向延伸,落点自然落在伙伴的正对面。
# 关键片段:镜像伙伴绕中心 X 移动 center = Vector(40, 34) partner = hero.findByType("peasant")[0] while True: vector = Vector.subtract(center, partner.pos) # 伙伴→中心的偏移 moveToPos = Vector.add(center, vector) # 中心另一侧的对称点 hero.move(moveToPos)
本关要走到红色 X,但直线过去会撞上牦牛。策略是经典的势场避障:始终有一个"奔向目标"的向量,一旦牦牛进入 10 米范围,就再叠加一个"远离牦牛"的向量,两者相加得到既朝目标、又绕开牦牛的合成方向。关键是每一步都必须整体朝目标推进,否则永远到不了。
目标向量用黄金三步造出:subtract(goalPoint, hero.pos) 求方向、normalize 归一化、multiply(goal, 10) 定长 10 米。避障向量只在 distance < 10 时才造:subtract(hero.pos, yak.pos)(从牦牛指向自己)、归一化、乘 10,得到 10 米的斥力。最后 Vector.add(goal, toYak) 把两者合成,再 add(hero.pos, goal) 算出落点并移动。
# 关键片段:目标向量 + 条件性避障向量 goal = Vector.subtract(goalPoint, hero.pos) goal = Vector.normalize(goal) goal = Vector.multiply(goal, 10) yak = hero.findNearest(hero.findEnemies()) if hero.distanceTo(yak) < 10: toYak = Vector.subtract(hero.pos, yak.pos) toYak.normalize() toYak.multiply(10) goal = Vector.add(goal, toYak) hero.move(Vector.add(hero.pos, goal))
本关要用球撞倒所有蓝骨架、不碰红骨架。要让球朝某个蓝骨架飞,农民必须站到球相对目标的"反方向"一侧去推——就像台球,想让球往前走,得从后面击打。所以关键是为每个蓝骨架算出正确的击球点(kickPoint)。
对每个蓝骨架 chip,先求"从球指向骨架"的方向:chip.pos.copy().subtract(ballPos).normalize()(用 copy() 避免改动骨架真实坐标)。这个单位向量指向骨架。击球点要在球的相反一侧,所以把方向乘以 -8 再加到球位上:Vector.add(ballPos, direction.multiply(-8))。农民走到这个点后再推向球,球就会朝骨架飞去。逐个处理完所有蓝骨架,并在每次击球后回到中心。
# 关键片段:为每个蓝骨架算出球背后的击球点 for chip in blueChips: direction = chip.pos.copy().subtract(ballPos).normalize() kickPoint = Vector.add(ballPos, direction.multiply(-8)) # 负号→球的另一侧 hero.command(kicker, "move", kickPoint) # ...等农民到位后,再命令它 move 向 ballPos 把球推出...
multiply(direction, -8) 里的负号让向量反向——原本指向骨架,取负后就指向骨架的相反侧。加到球位上,就得到"球的背后 8 米处"这个击球点。用正负号翻转方向,是向量运算里非常实用的小技巧。
ball.velocity.magnitude() 来判断球是否已经停回中心。幅值接近 0 表示球停住了,此时才好进行下一次击球,避免在球还在滚时乱推。
本关要在食人魔进三球前先得三分。它融合了本单元几乎所有技能:用旗帜动态指挥球员跑位、用"找最近单位"的擂台算法选人、用向量算出正确的击球点把球射向前方。这是一关综合演练。
第一段·旗帜调度:每帧 findFlag("green"),若有旗,就用"最近距离擂台"找出离旗最近的球员,命令它 move 到旗位,然后 removeFlag 清除旗帜。第二段·选控球者:再用一次擂台找出离球最近的球员 closestFriend。第三段·向量击球:当控球者站在球的正确一侧且足够近时,用 subtract 求出击球方向、normalize、multiply(3) 定长,再 add 出击球点,命令球员移过去把球顶向前方球门。
# 关键片段:旗帜调度最近球员 flag = hero.findFlag("green") if flag: closest = None closestDist = 9999 for friend in friends: if friend.distanceTo(flag) < closestDist: closest = friend closestDist = friend.distanceTo(flag) if closest: hero.command(closest, "move", flag.pos) hero.removeFlag(flag)
goalPoint = Vector(ball.pos.x - 3, ball.pos.y) 之类的计算,让球员绕到球的一侧再推,就能把球送向正确方向。多试几次找手感——正如关卡玩笑说的,"至少游戏很快就会结束"。
这是本单元的高难挑战关,是前面所有技能的集大成。农民像贪吃蛇一样收集硬币,身后拖出不断变长的尾巴,一旦碰到尾巴(或墙、或别的蛇)就会被摧毁,要凑够 200 金币。策略是把第三单元的价值/距离选币和本单元的向量避障结合起来——朝最划算的硬币去,同时用斥力向量绕开自己的尾巴。
选币:用 findBestItem 按 value / distance 打擂台选出性价比最高的硬币。造吸引力:subtract(coin.pos, friend.pos) → 归一化 → 乘 7,得到奔向硬币的向量 goal。造斥力:读取农民身后的 friend.snakeBackward(尾巴),用 subtract(friend.pos, tail.pos)(从尾巴指向自己)→ 归一化 → 乘 7,得到远离尾巴的向量 toTail。合成:Vector.add(goal, toTail) 相加,再加回当前位置得到落点。这正是"滑走"避障模型在动态障碍上的复用。
# 关键片段:奔向硬币 + 远离自己的尾巴 coin = findBestItem(friend, coins) # 价值/距离选币 goal = Vector.subtract(coin.pos, friend.pos) goal = Vector.normalize(goal) goal = Vector.multiply(goal, 7) if friend.snakeBackward: # 身后的尾巴 tail = friend.snakeBackward toTail = Vector.subtract(friend.pos, tail.pos) toTail = Vector.normalize(toTail) toTail = Vector.multiply(toTail, 7) goal = Vector.add(goal, toTail) # 合成方向 moveToPos = Vector.add(friend.pos, goal) hero.command(friend, "move", moveToPos)
add 进合成方向里。
subtract(目标, 自己),远离障碍用 subtract(自己, 障碍);写反会朝相反方向移动。normalize 就 multiply,长度会失控,移动步长忽大忽小。chip.pos 等真实坐标做运算前要先 .copy(),否则会污染单位的实际位置。atan2(y, x)(先 y 后 x),写成 atan2(x, y) 角度全错。findFlag 要 removeFlag,否则每帧重复触发、逻辑卡死。add(自己.pos, goal) 得到真正的落点才能 move。这是安息云峰上分量最重的一个单元,你掌握了控制"往哪走、走多远"的完整向量语言!你学会了用 Vector.subtract 求两点间的方向与距离向量(牢记"目标减自己");用 Vector.add 合成位置与叠加向量;用 Vector.normalize 把向量压成长度 1 只留方向、再用 Vector.multiply 缩放到想要的长度——这"黄金三步"是所有移动的基石;你还理解了把"吸引力"和"斥力"相加的避障算法(势场思想),会用 Math.atan2 求方向角度,并用 findFlag、removeFlag 动态指挥单位。从绕圈的镜像、滑走的避障、精准逆转与冰球的击球,到蛇法储蓄的动态避障,你把向量运算用得淋漓尽致。来回顾一下核心概念:
| 核心概念 | 关键点 | 对应关卡 |
|---|---|---|
| subtract 求方向 | 目标减自己 / 自己减障碍 | 绕圈·滑走 |
| add 合成位置 | 当前位置 + 位移 = 落点 | 绕圈 |
| normalize 归一化 | 保方向、长度变 1 | 滑走 |
| multiply 缩放 | 长度乘倍数(负号可反向) | 滑走·精准逆转 |
| 避障算法 | 目标向量 + 远离向量 相加 | 滑走·蛇法储蓄 |
| Math.atan2 求角 | 参数顺序 (y, x) | 精准逆转·冰球 |
| findFlag/removeFlag | 读旗调度、用完即删 | 冰球 |
| 综合避障 | 选币吸引 + 尾巴排斥 | 蛇法储蓄 |
懂得用向量精确表达方向与距离、能把吸引与排斥合成为一条智慧路线的英雄,才是真正驾驭空间与运动的云峰智者。
继续向雪山之巅挺进吧,勇敢的程序员!🏔️➡️🧲
扣哒世界 编程知识 · 计算机科学 6(安息云峰):第四单元
扣哒世界教研团队