AI 门道 · AI 资讯 · 学习中心 · 模型与平台 · 工具导航

Godot + AI 实战:从零做一个可重开的躲避游戏

中文逐步实战:建工程、画角色、实现移动与敌人、检测碰撞、计时并重开。附可导入工程与完整GDScript。

示例运行画面

Godot 4.x;本机验证4.7.2 · 约60–90分钟;首次使用引擎请额外预留安装时间

已通过Godot 4.7.2无窗口运行检查:碰撞结束、重开归零、离屏清理。桌面导出与手机真机未验证。

下载完整源码与中文说明

01 · 先看成品规则与工程结构

本例是一局不断下落红色圆点的躲避游戏。玩家是底部绿色圆点,按左右方向键移动;手机触摸时朝手指横坐标移动。碰到红点立即结束,显示存活秒数,点击按钮重新开始。 这是本站原创的代码绘图练习,不依赖图片、音乐或付费素材,也不是官方Dodge the Creeps项目的翻译。先做一个单场景闭环,再拆成多个场景,能减少初学者在资源绑定上的干扰。 下载工程直接导入project.godot可以先体验完整结果;想从零学习,请按下面步骤创建同样的文件。工程只有project.godot、main.tscn、main.gd,以及供维护者运行的test.gd。

02 · 创建项目和唯一主场景

使用Godot 4.x标准版,本例实际检查版本为4.7.2。新建项目,渲染器选兼容模式。创建Node2D根节点,命名Main,保存为main.tscn。给根节点附加脚本main.gd。 在项目设置→显示→窗口中,把视口宽度设为640、高度720。第一次按F6运行当前场景,应该看见空窗口;按F5运行整个项目时选择main.tscn为主场景。不要把脚本挂到一个叫Main的普通文件夹上。 不想手工设置时,可以直接使用下载包里的工程配置;无需安装任何AI或MCP插件。

03 · 先理解状态,避免把画面当成数据

player保存玩家中心坐标;enemies是敌人坐标数组;elapsed是本局累计时间;spawn_left是距离下一次生成的倒计时;running决定是否推进游戏。 坐标原点在左上角,x向右、y向下。玩家从(320,620)开始,敌人在y=-20生成、向下移动。角色半径18,敌人半径15,所以两者圆心距离小于33就算碰撞。 本例使用几何碰撞教学,没有接物理引擎。将来换成复杂地图时应评估Area2D或CharacterBody2D;不要把圆形距离判定当成任何游戏都适用的碰撞方案。

04 · 完整主脚本:复制到main.gd

把默认脚本全部替换为下面这份代码,保存后按F6。第一屏应看见绿色玩家和开始按钮;点击开始,红色圆点开始下落。下面代码与下载工程的main.gd保持一致。 如果复制时缩进变成空格与Tab混用,先在脚本编辑器统一缩进,再看底部第一条解析错误。

extends Node2D

const SIZE := Vector2(640, 720)
const PLAYER_RADIUS := 18.0
var player := Vector2(320, 620)
var enemies: Array[Vector2] = []
var elapsed := 0.0
var spawn_left := 0.0
var running := false
var touch_x := -1.0
var rng := RandomNumberGenerator.new()
var hud := Label.new()
var button := Button.new()

func _ready() -> void:
	rng.randomize()
	hud.position = Vector2(24, 18)
	hud.add_theme_font_size_override("font_size", 24)
	add_child(hud)
	button.position = Vector2(210, 300)
	button.size = Vector2(220, 64)
	button.text = "开始 / 重新开始"
	button.pressed.connect(start_game)
	add_child(button)
	update_hud()
	queue_redraw()

func start_game() -> void:
	player = Vector2(320, 620)
	enemies.clear()
	elapsed = 0.0
	spawn_left = 0.4
	touch_x = -1.0
	running = true
	button.hide()
	update_hud()
	queue_redraw()

func _input(event: InputEvent) -> void:
	if event is InputEventScreenTouch:
		touch_x = event.position.x if event.pressed else -1.0
	elif event is InputEventScreenDrag:
		touch_x = event.position.x

func _process(delta: float) -> void:
	if not running:
		return
	# 暂时离开窗口后,不让一次大步进跳过碰撞。
	var dt := minf(delta, 0.05)
	elapsed += dt
	var axis := Input.get_axis("ui_left", "ui_right")
	if touch_x >= 0.0:
		player.x = move_toward(player.x, touch_x, 360.0 * dt)
	else:
		player.x += axis * 360.0 * dt
	player.x = clampf(player.x, PLAYER_RADIUS, SIZE.x - PLAYER_RADIUS)
	spawn_left -= dt
	if spawn_left <= 0.0:
		enemies.append(Vector2(rng.randf_range(20.0, 620.0), -20.0))
		spawn_left = maxf(0.18, 0.65 - elapsed * 0.006)
	for i in range(enemies.size() - 1, -1, -1):
		enemies[i].y += (200.0 + elapsed * 3.0) * dt
		if enemies[i].distance_to(player) < PLAYER_RADIUS + 15.0:
			running = false
			button.show()
			break
		if enemies[i].y > SIZE.y + 20.0:
			enemies.remove_at(i)
	update_hud()
	queue_redraw()

func update_hud() -> void:
	hud.text = "存活 %.1f 秒  |  方向键 / 触摸移动" % elapsed
	if not running and elapsed > 0.0:
		hud.text = "本局 %.1f 秒 · 碰到了!再试一次" % elapsed

func _draw() -> void:
	for x in range(0, 640, 40):
		draw_line(Vector2(x, 70), Vector2(x, 720), Color(0.10, 0.15, 0.22))
	draw_circle(player, PLAYER_RADIUS, Color(0.36, 0.94, 0.77))
	for enemy in enemies:
		draw_circle(enemy, 15.0, Color(1.0, 0.43, 0.39))

05 · 移动为什么要乘delta

速度360的单位是像素/秒。每帧移动量等于速度乘本帧秒数;如果直接每帧加360,移动速度会随着帧率变化。Input.get_axis使用Godot预设的ui_left与ui_right,下载工程无需自定义输入动作。 clampf把x限制在[18,622],使圆形角色不越界。触摸路径用move_toward逐步靠近手指,而不是瞬移。先只验收左右移动和边界,再考虑加入加速度。 代码将过大的delta限制到0.05秒,避免从后台回到窗口时跳过大量碰撞。本局时间按实际模拟步进累计,刻意不当作严格墙上时钟。

06 · 敌人生成、清理与碰撞怎么串起来

每帧减少spawn_left,归零就往数组加入一个位置。初始间隔0.65秒,随着存活时间逐渐缩短,最小0.18秒;下落速度也缓慢增加。 遍历数组时从最后一个元素往前走,因为remove_at会让后面的元素前移。正向删除容易漏处理邻近元素。敌人离开窗口后从数组删除,防止一局越玩数据越多。 碰撞后先把running设为false,再显示按钮并退出本次敌人循环。下一帧_process一开始直接return,所有敌人停止移动,不会出现失败后仍继续计时的问题。

07 · 为什么重开不用重新加载整个项目

start_game集中重置玩家坐标、敌人数组、计时器和触摸目标,再隐藏按钮。按钮的pressed信号只在_ready连接一次,所以每局不会多注册一个回调。 测试方法:故意撞到敌人后连续重开5次,观察第一颗敌人的出现时间与玩家速度是否一致。若重开越来越快,优先检查是否重复创建Timer、连接信号或保留了上局状态。

08 · 用AI做一次有验收标准的改动

不要让AI“把游戏优化一下”。先提交main.gd和实际版本,只要求增加一个机制,例如3点生命值。可以复制下面的任务;修改前保留原文件,完成后对照每项验收。 如果AI生成的代码报错,把第一条错误、行号和相关函数发回去,不要只说“不能用”。本练习不需要发送API密钥或其他项目文件。

项目是Godot 4.7.2,main.gd如下【粘贴】。请增加3点生命值和受击后1秒无敌:保持现有输入与坐标体系;同一帧撞到多颗敌人只扣1点;剩余生命为0才结束;重开恢复3点生命。请先列修改位置,再给代码和5条可操作验收步骤。不要引入插件。

09 · 验收、排错与导出

验收:①未开始前不计时;②左右移动不越界;③红点从上方进入;④碰撞后时间冻结;⑤重开清空敌人并归零;⑥离屏敌人不再保留。下载包test.gd自动检查碰撞、重开和离屏清理,不能代替视觉和真机测试。 看不到按钮:检查main.gd是否挂在Main节点;无法左右移动:先点击运行窗口获取焦点,确认没有改动ui_left/ui_right;只出现空场景:确认F5所选主场景。 导出桌面程序时先通过编辑器安装匹配版本的导出模板,再在项目→导出选择目标系统。本文没有附导出二进制,也没有声称完成Android/iOS真机适配。

godot --headless --path ./godot-dodge --script test.gd

配套工具与教程

    资料来源