Kraken Dash 这个项目,最初就是我在周末用一个下午加一个晚上塞出来的一个小游戏原型。起因很简单:我想验证 Godot 4 在 2D 横版无限跑酷类型上能做到多省事,同时又想避免再做一堆“复古像素跳台”的模板。海洋、巨妖、冲刺,这三个词拼在一起,玩法就立刻清晰了——玩家控制一头年轻的海怪在深海中持续向右冲刺,上下闪避障碍、收集珍珠,必要时消耗能量发动一次“触手冲刺”,直接撞碎挡路的礁石。这篇文章就把整个项目的设计思路、核心实现和踩坑过程完整写一遍,如果你也对独立游戏开发、Godot 引擎或者海洋主题关卡感兴趣,可以直接抄作业。
1. 项目概述与方案选型
1.1 核心需求与目标
Kraken Dash 从一开始就不是奔着“3A大作”去的。我的核心需求只有三个:第一,做出一款能让人打开就玩、十秒内明白规则的小游戏;第二,把“冲刺破坏”这个动作做得足够爽;第三,验证 Godot 4 在程序化生成、无限卷轴、碰撞检测这些常见 2D 玩法上的整体效率。换句话说,这是一个技术验证和玩法验证并行的原型项目。
不用动不动就规划几十个小时的任务清单。我给自己定的目标非常具体:第一天晚上完成基本移动和跳跃层面的操作,第二天上午把障碍生成、碰撞死亡、分数显示串起来,第二天下午补上冲刺特效、音效和游戏结束界面。实际完成度比计划高一点,最后还加上了简单的背景卷轴和珍珠收集系统。
这个项目适合谁参考?我的看法是,适合刚接触 Godot 的新手,也适合想从零快速搭一个跑酷类玩法骨架的老手。如果你是第一次做游戏,最好先把官方文档的 2D 入门教程刷一遍,再来看这篇文章,否则一些节点概念会有点晕。如果你已经做过几个小项目,那可以直接把代码抄走,改一改就能变成自己想要的版本。
1.2 技术方案选型
技术选型上我没太多犹豫。引擎直接用 Godot 4.2,语言用 GDScript,因为 Godot 4 对 2D 的支持非常顺手,内置的 TileMap、Parallax2D、CharacterBody2D 都是现成的,而且整个编辑器只有几百 MB,不像有些引擎光启动就要等半天。
物理模块我用了 CharacterBody2D 作为玩家角色,障碍物统一用 Area2D 做检测。为什么不直接用 StaticBody2D?因为“冲刺击碎障碍”这个机制需要动态判断碰撞对象,Area2D 的信号回调比刚体间的碰撞回调更灵活,我可以随时开启或关闭监控区域,不用频繁切换图层碰撞规则。
图形资源这块,我没有外包美术,也没有下载一堆风格不统一的素材包,而是用 Godot 自带的 Polygon2D、Line2D 和简单的自绘制纹理拼出来。海洋场景其实非常适合程序化处理:深蓝渐变背景、随机的气泡、波浪线、简单的鱼群剪影,这些用代码画出来的效果比素材包更统一,性能也更好。
音效方面,我没有用外部音频文件,全部用 AudioStreamGenerator 在运行时生成。冲刺声就是一段白噪声加低通滤波,收集珍珠声就是一个短促的正弦波“叮”。省掉了找素材的麻烦,也避免了版权问题。
1.3 目标用户与体验定位
Kraken Dash 的目标用户很明确:喜欢休闲但又不完全佛系的玩家。它的操作只有两个维度:上下移动和按冲刺键。上手难度几乎为零,但要想跑得远,就必须学会在正确的时机用冲刺去击碎可破坏障碍,这就给了玩家一个“练习、突破、再来一局”的理由。
单局时长我控制在三到八分钟。太快会让人没有成就感,太慢又会让人觉得像在上班。我参考了很多跑酷游戏的设计节奏:前三十秒几乎没有任何压力,纯粹让玩家熟悉手感;三十秒后障碍密度逐渐提升;到了两分钟以后,生成间隔会稳定在一个“高手看起来还能应对,新手会手心冒汗”的平衡点。
体验定位上,我希望玩家感受到的是“速度”和“破坏感”,不是“压迫感”。所以背景颜色亮度偏高,水流粒子的密度不搞得太夸张,被冲刺击碎的障碍会爆成小碎片,而不是让屏幕变成一片红。撞到障碍时也不用特别血腥,就是屏幕一抖、角色变成半透明、然后重开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心玩法设计拆解
2.1 核心循环
核心理念很简单:玩家在一条横向卷轴的海底通道中自动前进,控制 Kraken 上下移动躲避障碍物,同时收集散落的珍珠。珍珠不仅是分数,还是“冲刺能量”。
这个循环就是:不断前进 -> 收集珍珠 -> 消耗冲刺能量击碎障碍 -> 跑得越远分数越高 -> 撞上障碍死亡 -> 看到自己的最高分 -> 再试一次。
为什么要设计成“自动前进”?这是很多人做跑酷容易搞错的地方。如果让玩家控制前进速度,那大多数人的第一反应是“慢一点,再慢一点”,最后变成一个慢性拖延游戏,完全失去冲刺的感觉。自动前进把速度控制权交还给了关卡设计,我可以决定玩家在某一秒面对什么障碍密度,而不是把节奏交给玩家的胆量。
珍珠为什么能积攒冲刺能量?因为我想给玩家一个正反馈循环。单纯躲避障碍是负反馈机制,跑酷游戏玩久了容易累;添加收集物后,玩家会在躲避的同时主动寻找珍珠路线,相当于给每一局注入了“探索”和“规划”的味道。
2.2 难度曲线与障碍物设计
障碍物我做了四种,每种应对思路都不一样。静态岩石是最基础的障碍,只能绕开,考验的是基础移动。移动水母会上下浮动,给你的移动造成时间差,需要观察规律再通过。尖刺墙是连续的多段伤害区域,通常搭配窄通道出现,考验连续闪避能力。可破坏礁石是专门给冲刺机制准备的,撞击即碎,会产生大量的碎片粒子,视觉反馈最爽。
纯随机生成是最偷懒也最容易出问题的方案。我一开始真试过,每间隔 0.6 秒随机在几个固定高度生成障碍,结果经常出现两个障碍物之间的距离太短,玩家根本没有反应时间。后来我把生成改成了“最小间隔 + 随机浮动”的思路:
gdscript复制# 根据当前分数调整基础间隔
var base_interval := clamp(0.65 - score / 6000.0, 0.35, 0.65)
var jitter := randf_range(-0.08, 0.08)
spawn_timer.wait_time = base_interval + jitter
spawn_timer.start()
这段逻辑的核心是:难度增长来自最小间隔的逐步缩短,而不是单纯靠运气增加障碍密度。随机浮动只负责制造“不完全重复”的体验,不会制造无解局面。
障碍物的纵向分布也需要控制。我会把可用的垂直空间分成五条车道,每条车道高度大约 96 像素。生成时先从五条车道里随机选两到三条,再把障碍物放置进去。之所以不直接给随机坐标,是因为连续两个障碍可能落在同一车道,玩家会被迫做一次非常刁钻的急转弯,体验很差。
2.3 冲刺能力与“Kraken Dash”主题结合
冲刺是这个项目的灵魂,也是标题里“Dash”的直接体现。在我最初的方案里,它只是一个普通无敌技能,按一下就能穿墙,后来试玩发现很容易变成“无脑按空格就能横扫一切”,反而没意思。
调整后的设计是:冲刺有 1.2 秒冷却,持续 0.18 秒,期间不仅无敌,还会在前方生成一个扇形触手区域,任何被碰到的可破坏礁石都会立刻碎裂。冷却时间的存在要求玩家在“普通障碍靠走位,可破坏障碍靠冲刺”之间做决策,而不会一路莽过去。
我把这个机制叫做“Kraken 血脉觉醒”,表达上有一种传说生物的味道。发动冲刺的瞬间,玩家周围会炸开一圈水花,触手向正前方猛扑出去,屏幕轻微震动,背景速度临时提高 1.5 倍,持续 0.3 秒,结束后再恢复。这样玩家会有一种“我真的在深海里冲了一下”的速度错觉。
参数上,我专门做了一个小表记录实测后的手感调整:
| 参数 | 初版值 | 最终值 | 调整原因 |
|---|---|---|---|
| 冲刺冷却 | 2.0 秒 | 1.2 秒 | 初版节奏太慢,玩家经常冷却没转好就撞墙 |
| 冲刺持续 | 0.30 秒 | 0.18 秒 | 初版无敌时间太长,难度体系失效 |
| 背景增速 | 2.0 倍 | 1.5 倍 | 2.0 倍会让移动判断出现偏差,撞墙率飙升 |
| 触手扇区角度 | 60 度 | 90 度 | 60 度太窄,可破坏障碍稍微偏一点就打不到 |
这些数值不是一口气调出来的,而是反复试了十几局才定下来。我的原则是:冲刺一定要“能救命但也要讲究时机”,否则玩家不会去理解障碍类型差异。
3. 实操过程与核心环节实现
3.1 节点结构与场景组织
打开 Godot 4 后先建一个空场景,根节点我用 Node2D,命名为 GameScene。为什么不直接用主场景 Main?因为我想让整个项目在多人协作或者后续扩展时更清晰,GameScene 这个名字一眼就能看出是玩法入口。
节点树大致长这样:
text复制GameScene (Node2D)
├── World (Node2D)
│ ├── Background (Parallax2D)
│ ├── Player (CharacterBody2D)
│ ├── ObstacleSpawner (Node2D)
│ └── CollectibleSpawner (Node2D)
├── UI (CanvasLayer)
│ ├── ScoreLabel (Label)
│ ├── DashCooldownBar (ProgressBar)
│ └── GameOverPanel (Control)
World 节点是纯粹的容器,专门放所有物理对象。这样做的好处是,后面如果要做摄像机震动、全局暂停、调试用碰撞可视化,只需要对 World 这一个节点做操作,不用逐个改子场景。
UI 单独放在 CanvasLayer 下,保证界面永远在最上层,不受玩家移动和摄像机影响。分数标签用 Label,冲刺冷却条用 ProgressBar,游戏结束面板用一个隐藏的 Control,死亡时 show 出来。
场景组织里最容易踩的坑是:把 UI 节点塞进了 World。结果就是游戏一旦添加摄像机跟随,UI 也跟着动,甚至会被背景遮住。凡是想在屏幕上固定显示的,都必须放到 CanvasLayer 下面。
3.2 玩家角色:移动、冲刺与碰撞
Player 是一个 CharacterBody2D,挂了 CollisionShape2D,形状是一个半径 28 的圆形。圆形碰撞体在 2D 游戏里最友好,各方向的判定距离一致,不会出现方块碰撞体“明明没碰到却死了”的棱角问题。
移动逻辑没有用重力。海底场景本身可以理解为一种“低重力”空间,我不想引入跳跃和重力,因为跑酷游戏一旦跳跃,手感调起来就复杂很多倍。现在只做垂直移动,响应更直接,也更适合手机上的滑动操作。
gdscript复制extends CharacterBody2D
@export var move_speed := 280.0
@export var dash_speed := 700.0
@export var dash_duration := 0.18
@export var dash_cooldown := 1.2
@onready var dash_area := $DashArea
@onready var dash_timer := $DashTimer
@onready var cooldown_timer := $CooldownTimer
var is_dashing := false
var can_dash := true
func _physics_process(delta):
var input_dir := Input.get_axis("ui_up", "ui_down")
velocity.y = lerp(velocity.y, input_dir * move_speed, clamp(delta * 12.0, 0.0, 1.0))
move_and_slide()
if Input.is_action_just_pressed("dash") and can_dash:
_start_dash()
func _start_dash():
is_dashing = true
can_dash = false
dash_area.monitoring = true
dash_timer.start(dash_duration)
cooldown_timer.start(dash_cooldown)
func _on_dash_timer_timeout():
is_dashing = false
dash_area.monitoring = false
func _on_cooldown_timer_timeout():
can_dash = true
func _on_dash_area_body_entered(body):
if body.is_in_group("breakable"):
body.break_and_score()
移动这行我用了 lerp 对目标速度做插值,而不是直接赋值。这样手感会有一种“在水中滑动”的惯性,不是硬邦邦地瞬移。数值 12.0 是实测出来的,太高会失去惯性,太低会让操作像在浆糊里游泳。
冲刺做法是在玩家节点下再挂一个 Area2D,命名为 DashArea。这个区域是一个朝右偏移 30 像素、宽度 90 像素、高度 100 像素的矩形。平时 monitoring = false,只有冲刺时打开。用这种区域检测的方式比“让玩家角色物理上真的向前冲一段距离”更稳定,因为它是瞬时的,不会和世界卷动冲突,也不会把玩家撞到下一个障碍堆里。
顺带一提,我还给 Player 加了一个 is_dashing 状态,用来在动画系统里播放触手扩张效果。状态本身很简单,不给它加复杂的状态机,不然会出现“状态覆盖”导致技能放不出来的问题。
3.3 程序化障碍生成与回收
ObstacleSpawner 是一个 Node2D,内部挂一个 Timer,初始等待时间 0.8 秒,启动后立刻连接 timeout 信号。每次超时生成一波障碍物,然后在玩家到达安全距离前重新设置等待时间。
这里的关键是“生成位置必须在屏幕右侧外”,不要让障碍物凭空出现在玩家脸上。我通过引用玩家节点来拿到当前 x 坐标,统一用一个 spawn_x = player.global_position.x + 900 作为生成线,这个距离刚好是屏幕宽度多一点,玩家可以看到障碍物缓缓飘过来,有反应时间。
gdscript复制extends Node2D
@export var player: Node2D
@export var obstacle_pool: Node2D
@export var rock_scene: PackedScene
@export var jellyfish_scene: PackedScene
@export var spike_scene: PackedScene
@onready var spawn_timer := $SpawnTimer
var score := 0
func _ready():
spawn_timer.timeout.connect(_spawn_wave)
func _spawn_wave():
var chosen_type = _choose_type()
var obstacle = _get_obstacle(chosen_type)
obstacle.position = Vector2(player.global_position.x + 900, _get_valid_y())
_configure_difficulty(obstacle)
对象池是我在第三个版本才加进来的。最初直接用 instantiate() 生成,障碍物离开屏幕后调用 queue_free() 销毁。看起来没问题,但玩到两分钟以后,每局会创建几百个节点,Godot 的 GC 会有可见的卡顿。改用对象池后,性能稳定多了。
实现方式很土但很有效:提前创建十几个空节点挂在 obstacle_pool 下,每种障碍物预置三到五个实例,初始全部隐藏。生成时从池子里拿一个出来,设置位置、显示;离开屏幕后放回池子,重置所有状态。简单代码示意:
gdscript复制func _get_obstacle(type: PackedScene) -> Node2D:
# 先从池子里找已隐藏的实例,找不到就 instantiate 新的
for child in obstacle_pool.get_children():
if child.scene_file_path == type.resource_path and not child.visible:
child.visible = true
return child
var new_obstacle = type.instantiate()
obstacle_pool.add_child(new_obstacle)
return new_obstacle
这里最需要注意的就是 scene_file_path 的匹配。PackedScene 实例化出来的节点不一定自带这个属性,最好是用 is_in_group 或者加一个自定义字段来区分类型,不要依赖路径字符串。
3.4 无限背景与卷轴效果
海底背景我用了两层 Parallax2D。一层是远处的深海鱼群剪影,滚动速度设定为玩家前进速度的 0.3 倍;一层是近处的水草和气泡,速度是 0.7 倍。两层叠加后就有一种“玩家在高速移动,但周围世界明显分层次”的感觉。
Parallax2D 在 Godot 4 里可以直接设置 repeat_size,然后通过改变 scroll_offset.x 实现无缝循环。我这里贴一个核心代码:
gdscript复制extends Parallax2D
@export var scroll_speed := 120.0
func _ready():
repeat_size.x = 1920
scroll_offset.x = 0.0
func _process(delta):
scroll_offset.x -= scroll_speed * delta
if scroll_offset.x <= -repeat_size.x:
scroll_offset.x += repeat_size.x
气泡粒子是另一种动态元素。我做了十几个半透明的圆形,在气泡节点下随机生成,向上飘,飘出屏幕顶部后回到底部。这个本来可以用 GPUParticles2D 做,但考虑到网页导出兼容性,我选择了简单的脚本生成。数量不大,性能影响可以忽略。
背景不能做得太花哨。我试过把背景鱼群做成实体精灵,结果它们和障碍物出现在同一层,玩家很容易误判障碍距离。最后所有背景元素都放在 layer = -1,并且关闭了碰撞层,这样它们只负责营造氛围,不干扰操作判断。
3.5 分数、UI 与游戏状态管理
分数计算有两种常见方案:按收集物加分,或者按生存时间加分。我两个都用了。玩家每收集一颗珍珠加 50 分,每在游戏中存活 0.5 秒加 1 分。显示的时候只显示总分,玩法上没有区分来源,但内部统计会记录“珍珠数”和“生存时间”,方便以后加排行榜。
UI 上的核心是右侧的冲刺冷却条。它连接着冲刺的状态,冷却完成时会有一个“充满”的动画效果。我用 ProgressBar 的 value 从 0 到 1 来回变化,结合 Tween 做丝滑过渡:
gdscript复制func _on_cooldown_timer_timeout():
can_dash = true
var tween = create_tween()
tween.tween_property(dash_bar, "value", 1.0, 0.15)
游戏状态我用最原始的枚举管理:
gdscript复制enum GameState { MENU, PLAYING, GAMEOVER }
var current_state := GameState.MENU
状态切换时做三件事:重置玩家位置和速度,清空所有障碍物池子,暂停或恢复 Timer。为什么不用 Godot 的 pause 模式?因为场景里有些动画我想让它继续播放,比如背景水波和气泡,暂停整个树反而要多写处理逻辑。直接用枚举控制,简单直白。
死亡瞬间我还加了一个 0.6 秒的慢动作效果,用 Engine.time_scale = 0.3,再配合一个 Tween 恢复回 1.0。这招在很多动作游戏里是标配,制造一种“一切变慢,你能看清自己是怎么死的”的感觉。
4. 常见问题与排查技巧实录
4.1 手感与碰撞体调优
做跑酷游戏,手感问题是排在第一位的。最常见的情况是“玩家抱怨明明躲开了还是死了”。我排查过后发现,绝大多数问题出在碰撞体比视觉图形大很多。
我用圆形碰撞体,视觉上 Kraken 本体是一个半径 34 像素的圆,碰撞体给的半径只有 22 像素。为什么故意缩小?因为玩家在高速移动时,眼睛看到的“可以安全穿过”的位置和实际物理判定会有偏差,留出 12 像素的宽容度能大幅降低挫败感。
反之,障碍物的碰撞体我会比视觉小 10% 到 15%。比如尖刺墙视觉上是从天花板延伸到地板的整根尖刺,但碰撞区只取中间 70% 的部分。这样玩家能获得“擦着边过去了”的反馈,这种侥幸感会让游戏更有趣。
垂直移动手感是我调了很久的地方。初版直接 velocity.y = input_dir * move_speed,结果玩家一抖一抖的,没有水中滑行的感觉。改成 lerp 插值后还是不够顺滑,最后给 move_speed 做了一个“起步和停止”的加速度曲线:
gdscript复制var target_y = input_dir * move_speed
velocity.y = move_toward(velocity.y, target_y, acceleration * delta)
acceleration 我设的是 2000,这个数值意味着从静止到全速大约需要 0.14 秒。太快会显得生硬,太慢会让人感觉操控延迟。
4.2 随机生成与难度平衡
随机生成最大的坑是“看似随机,实则灾难”。纯随机会在极低概率下生成三个连续障碍物,把玩家唯一的逃生路线堵死。玩家遇到这种局面不会觉得“运气不好”,只会觉得游戏设计有问题。
我的解决方式是引入“车道阻塞检查”。生成下一波时,检查未来 400 像素范围内已经被占据的车道,如果上一波占用了车道 1 和 3,这一波就强制避开 1 和 3,至少留一条完整的纵向空道。
实现上不用搞太复杂的算法,直接给每次生成加一个简单的历史记录:
gdscript复制var last_lanes := [false, false, false, false, false]
func _get_valid_y() -> float:
var lane := randi_range(0, 4)
var tries := 0
while last_lanes[lane] and tries < 3:
lane = randi_range(0, 4)
tries += 1
# 记录当前波次占用的车道
last_lanes = [false, false, false, false, false]
last_lanes[lane] = true
return lane_height(lane)
这个方案不完美,但在实际游玩中已经能避免绝大部分“必死”组合。如果想让难度更精确,可以升级成“波次模板”系统,预设几组合法布局,随机切换。后期我计划这样做。
4.3 性能优化实战
性能问题是无限跑酷的隐藏魔王。再简单的机制,只要跑上十分钟,场景里可能就有几百个活动节点。如果不控制,真机发热、网页变卡都会出现。
我用了三个层次的优化。第一层是对象池,前面已经讲过了,负责减少节点创建销毁的频率。第二层是离屏回收,用 VisibleOnScreenNotifier2D 挂在障碍物场景根节点上,当 screen_exited 信号触发时,不直接销毁,而是隐藏并回到对象池。第三层是关闭不必要的物理处理,游戏结束后我会调用 obstacle_pool.set_physics_process(false),让所有障碍物的隐形移动计算全部停掉。
粒子效果也要节省。礁石碎裂的碎片粒子我限制了单个障碍物最多产生 12 个,而不是想当然地生成 30 个。粒子数量一多,任何引擎都救不了。
最后,网页导出上我特意检查了 project.godot 里的渲染设置。Godot 4 默认的渲染器是 Forward+,在移动设备或浏览器上并不友好。我把项目的渲染方法改成了 gl_compatibility,画面效果损失一点,但兼容性和帧率明显提升。如果你用 Godot 4.2 以上版本导出 HTML5 发现画面全黑,多半就是这里的问题。
4.4 平台适配与导出问题
Kraken Dash 最初按桌面端设计,用方向键和空格。导出到网页后,移动端用户没法玩,所以我加了一套简易触摸控制:屏幕左半区触控向上/向下滑动时,映射到方向键上下;屏幕右半区点击时,映射到冲刺键。
代码上用了 _unhandled_input 事件,但要注意触摸事件的层级。如果 UI 按钮把触摸事件吃掉了,游戏层就收不到。我的做法是:右侧冲刺按钮直接放在 UI CanvasLayer 上,用 Control 的 gui_input 信号处理;左半区不放任何 Control,保证触摸事件能穿透到游戏层。这样互不干扰。
音频加载是另一个容易忽略的点。HTML5 导出后,浏览器默认不允许自动播放音频,必须在用户第一次点击或按键之后再初始化音频。我在游戏开始界面加了个“点击开始”,然后在按钮回调里调用 AudioServer 的解锁逻辑,而不是在 _ready() 里直接播放音效。
屏幕适配用的是项目设置里的 display/window/stretch/mode = canvas_items,同时设了一个基础分辨率 1152x648,这样可以保证不同比例的屏幕下,UI 不会变形,世界坐标也不会错乱。如果直接使用物理像素坐标,在手机和 4K 屏上会出现完全不同的视野范围,很影响平衡性。
5. 项目后续扩展与我的个人体会
先把后续计划说清楚。Kraken Dash 目前只做到了一个核心循环,距离完整产品还差不少。我的短期目标是加一个“每日挑战”模式,每天生成同一个随机种子地图,所有玩家跑同一张图,拼最高分。这比单纯的分数榜有意思,因为玩家会针对固定地图研究最优路线。
长期计划里有一个 Boss 战的想法:每隔 1000 分出现一个远古守卫,玩家需要用冲刺撞碎它身上的护甲,护甲破碎后露出核心,再用珍珠能量释放一次大范围攻击才能击败它。这个设计能打破“无限跑酷到最后就是比拼反应”的单调循环,给玩家一个阶段性的目标。
不过我要提醒一句,不要急着把所有想法都塞进第一版。Kraken Dash 从想法到能玩只用了两天,就是因为我把范围死死控制在“移动、收集、冲刺、死亡、重开”这五个环节里。你说的所有新功能,都要在基础手感舒服了之后再考虑。
我个人的体会是,动作类小游戏的成败真的就在手感上,而手感不是靠设计文档写出来的,是反复试出来的。Kraken Dash 里面那个 1.2 秒的冷却时间,初版是 2 秒,后来发现玩家等待的时间太无聊,改成 1.5 秒,又发现玩家会忘了用冲刺,最后改成 1.2 秒才终于有了“技能不是底牌,而是常规行动”的感觉。类似的调整还有很多,每改一次参数,都要重新玩五局以上,感受它在不同难度阶段的实际效果。
最后再分享一个小技巧:你不需要真的画一套完整的 Kraken 形象再启动开发。先用一个圆形加几条触手线段,把核心玩法跑通。等到玩法稳定了,再回来画正式的精灵图,你会发现美术替换是非常简单的事情,而玩法修改才是最伤筋动骨的。希望这篇 Kraken Dash 的实战拆解能帮那些想做跑酷、做水下主题、或者单纯想用 Godot 4 练手的开发者少走几步弯路。
