BV1JuCrYcEvJ第一个2D游戏(仅展示代码)4.4版本
看不到代码请重新刷新此页面
源码已上传github
LJJ-711/Godot_MyOneGame_tutorial: BV1fuCrYFER2自己制作教程游戏,后期可以根据这个来修改
游戏场景
导入游戏素材

将文件夹拖入到文件系统中
游戏动画



帧率越高,动画播放就会越快

把玩家添加到主场景里

这里可以让玩家在主场景开始游戏时候自动播放动作

GD编程
报错相关
indented:跟缩进相关的报错
映射版本控制WSAD角色
extends CharacterBody2D
@export var move_speed : float = 50
func _process(delta:float) -> void:
velocity= Input.get_vector("left","right","up","down")*move_speed
move_and_slide()以下是整合在一个代码块中、附带逐行详细注释的解析版本,注释会清晰说明每行代码的语法含义、功能作用和核心逻辑:
# 1. 继承CharacterBody2D节点:让当前脚本拥有2D物理角色的所有属性和方法(如velocity、move_and_slide),是2D角色移动的基础
extends CharacterBody2D
# 2. 定义可导出的移动速度变量:
# @export:让变量在Godot编辑器的检查器面板可见,可可视化调整数值
# var:声明变量的关键字
# move_speed:自定义变量名(移动速度)
# : float:显式指定变量类型为浮点型(小数)
# = 50:设置变量默认值为50(单位:像素/秒,控制角色移动快慢)
@export var move_speed : float = 50
# 3. 定义帧更新回调函数:
# func:声明函数的关键字
# _process:Godot内置回调,游戏运行时每帧都会执行(比如60帧/秒就执行60次)
# (delta:float):函数参数,delta是帧间隔时间(秒),用于消除帧率对移动的影响
# -> void:声明函数无返回值
func _process(delta:float) -> void:
# 4. 计算角色移动速度向量:
# velocity:CharacterBody2D内置属性(Vector2类型),存储角色的移动方向和速度
# Input.get_vector():获取上下左右输入并返回归一化的方向向量(避免斜向移动速度翻倍)
# 参数依次是:左、右、上、下的输入映射名(需在项目设置-输入映射中配置对应按键)
# * move_speed:将方向向量乘以速度值,得到最终的移动速度
velocity= Input.get_vector("left","right","up","down")*move_speed
# 5. 执行角色移动:
# move_and_slide():CharacterBody2D内置方法,根据velocity执行移动,自动处理碰撞、重力(若设置)、地面检测等
# 无需手动处理delta,方法内部已做时间补偿,保证移动速度稳定
move_and_slide()补充关键细节:
Input.get_vector()的“归一化”作用:斜向(如同时按上+右)时,向量长度会被修正为1,避免斜移速度比单方向快(比如原本单方向速度50,斜向不归一化会变成≈70.7)。move_and_slide()是CharacterBody2D的核心移动方法,相比普通的position修改,它会自动处理碰撞反弹、贴墙移动等物理逻辑,适合玩家角色。若想让移动更平滑,也可将
_process换成_physics_process(物理帧回调,固定60次/秒,不受渲染帧率影响),逻辑无需改动。
碰撞体
代码块
extends CharacterBody2D
@export var move_speed : float = 50
@export var animator : AnimatedSprite2D
func _physics_process(delta:float) -> void:
velocity= Input.get_vector("left","right","up","down")*move_speed
move_and_slide()
# 如果速度为0,播放待机动画
if velocity == Vector2.ZERO:
animator.play('idle')
# 如果速度不为0,播放跑不动画
else:
animator.play('run')
move_and_slide()
添加碰撞体



信号和函数
添加史莱姆


主角

# 继承Godot内置的CharacterBody2D节点,获得2D物理角色的核心属性(如velocity)和方法(如move_and_slide),是2D可碰撞角色移动的基础
extends CharacterBody2D
# @export:导出变量,可在编辑器检查器面板可视化调整数值;var声明变量;move_speed是自定义变量名;:float指定浮点型;=50设置默认移动速度(像素/秒)
@export var move_speed : float = 50
# @export导出变量,animator是自定义变量名;:AnimatedSprite2D指定变量类型为2D动画精灵节点,用于控制角色动画播放(需在编辑器绑定对应节点)
@export var animator : AnimatedSprite2D
# 定义布尔类型变量is_game_over,用于标记游戏是否结束;bool值只有true/false两种状态;初始值设为false(游戏未结束)
# 注释补充:bool 变量:true或者false
var is_game_over : bool = false
# 定义Godot内置的物理帧回调函数,固定60次/秒执行(不受渲染帧率影响,适合物理相关逻辑)
# func声明函数;_physics_process是物理帧回调名;(delta:float)接收帧间隔时间参数;-> void表示函数无返回值
func _physics_process(delta:float) -> void:
# 条件判断:如果游戏未结束(is_game_over为false),才执行后续的移动和动画逻辑
if not is_game_over:
# 计算角色移动速度向量:通过Input.get_vector获取上下左右按键输入,返回归一化的方向向量(避免斜向移动速度翻倍),乘以move_speed得到最终速度,赋值给velocity(CharacterBody2D内置的速度属性)
velocity= Input.get_vector("left","right","up","down")*move_speed
# 执行角色移动:CharacterBody2D的核心移动方法,根据velocity移动,自动处理碰撞、地面检测等物理逻辑
move_and_slide()
# 条件判断:如果速度向量为零向量(Vector2.ZERO),说明角色静止,执行待机动画逻辑
if velocity == Vector2.ZERO:
# 调用动画精灵的play方法,播放名为'idle'的动画(需提前在AnimatedSprite2D中配置该动画)
animator.play('idle')
# 否则(速度向量不为零,角色在移动),执行跑步动画逻辑
else:
# 播放名为'run'的跑步动画(需提前配置该动画)
animator.play('run')
# 注:此处重复调用move_and_slide()属于冗余代码,建议删除一行,保留一次即可
move_and_slide()
# 自定义游戏结束函数,命名为game_over,无参数、无返回值,用于处理游戏结束的逻辑
func game_over():
# 将游戏结束标记设为true,后续_physics_process中的移动/动画逻辑会停止执行
is_game_over = true
# 播放名为"game_over"的结束动画(需提前配置该动画)
animator.play("game_over")
# 等待3秒:创建一个3秒的定时器,await暂停函数执行,直到定时器触发timeout信号(3秒后继续执行)
await get_tree().create_timer(3).timeout
# 重新加载当前场景:调用场景树的reload_current_scene方法,实现游戏重启
get_tree().reload_current_scene()冗余代码提醒:
_physics_process中出现了两次move_and_slide(),会导致角色移动逻辑执行两次(速度异常),需删除其中一行;动画配置要求:
idle/run/game_over这三个动画名,必须和AnimatedSprite2D节点中配置的动画名称完全一致,否则会播放失败;节点绑定要求:
animator变量需在 Godot 编辑器的检查器面板中,绑定场景内的AnimatedSprite2D节点,否则运行时会报空引用错误;输入映射要求:
left/right/up/down需在「项目设置 - 输入映射」中配置对应按键(如 A/D/W/S 或方向键),否则无法检测输入。
怪物

# 继承Godot内置的Area2D节点,Area2D是2D碰撞检测节点(无物理体,仅用于检测碰撞/重叠),适合做陷阱、敌人触发区等
extends Area2D
# @export:导出变量,可在编辑器检查器面板可视化调整数值;var声明变量;slime_speed是自定义变量名(史莱姆移动速度);:float指定浮点型;=-100设置默认值(负数值表示向左移动)
# 注:当前代码中该变量未被使用,属于冗余定义,可删除或替换硬编码的-100
@export var slime_speed : float = -100
# 定义Godot内置的物理帧回调函数,固定60次/秒执行(不受渲染帧率影响,适合物理/移动相关逻辑)
# func声明函数;_physics_process是物理帧回调名;(delta:float)接收帧间隔时间参数;-> void表示函数无返回值
func _physics_process(delta:float)->void:
# 控制节点位置移动:
# position是Node2D的内置属性(Vector2类型),表示节点在场景中的坐标;
# += 是赋值运算符,将原有坐标加上新的位移量;
# Vector2(-100,0) 表示X轴向左移动(负方向)、Y轴不动,数值100是基础移动速度(像素/秒);
# *delta 是乘以帧间隔时间,消除帧率差异(保证不同帧率下移动速度一致);
# 最终效果:节点以每秒100像素的速度向左持续移动
position += Vector2(-100,0)*delta
# 定义Area2D的内置回调函数,当有物理体(如CharacterBody2D)进入该Area2D的碰撞区域时触发
# func声明函数;_on_body_entered是Area2D的碰撞进入回调名;(body:Node2D)接收碰撞到的节点对象;-> void表示无返回值
# 注:该函数需要手动在编辑器绑定信号(Area2D的body_entered信号)才会生效
func _on_body_entered(body:Node2D) -> void:
# 条件判断:检测碰撞到的节点是否是CharacterBody2D类型(即玩家角色节点)
# is 是GDScript的类型判断关键字,用于验证节点的类型
if body is CharacterBody2D:
# 调用玩家角色节点的game_over()方法(需玩家脚本中定义该方法),触发游戏结束逻辑
body.game_over()冗余变量提醒:
slime_speed变量定义后未被使用,代码中硬编码了Vector2(-100,0),建议优化为Vector2(slime_speed,0),让速度可通过编辑器调整;信号绑定要求:
_on_body_entered函数需要手动绑定 Area2D 的body_entered信号才会触发,绑定步骤:选中场景中的 Area2D 节点 → 右侧「节点」面板 → 找到
body_entered信号 → 拖动到脚本节点 → 选择_on_body_entered函数;
碰撞层配置:需确保 Area2D 的「碰撞层 / 掩码」设置正确,能检测到玩家的 CharacterBody2D(比如玩家在 “玩家层”,Area2D 的掩码勾选 “玩家层”);
方法依赖:
body.game_over()要求玩家的 CharacterBody2D 脚本中必须定义game_over()函数,否则运行时会报 “没有该方法” 的错误;移动逻辑优化:当前移动是 “绝对速度”,若想让史莱姆速度可配置,可将
position += Vector2(-100,0)*delta改为position += Vector2(slime_speed,0)*delta。
extends Area2D
@export var slime_speed : float = -100 # 现在变量被实际使用
func _physics_process(delta:float)->void:
position += Vector2(slime_speed,0)*delta # 使用导出变量控制速度
func _on_body_entered(body:Node2D) -> void:
if body is CharacterBody2D:
body.game_over()Timer节点
player

以下是整合所有代码到一个代码块中,且每行下方附带详细注释的版本,清晰解析语法含义、功能逻辑和注意事项:
# 继承Godot内置的CharacterBody2D节点,获得2D物理角色的核心属性(velocity)和方法(move_and_slide),是可碰撞角色移动的基础
extends CharacterBody2D
# @export:导出变量,编辑器检查器可可视化调整;var声明变量;move_speed为自定义变量名;:float指定浮点型;=50设置默认移动速度(像素/秒)
@export var move_speed : float = 50
# @export导出变量;animator为自定义变量名;:AnimatedSprite2D指定类型为2D动画精灵节点,用于控制角色动画(需在编辑器绑定对应节点)
@export var animator : AnimatedSprite2D
# 注释说明布尔变量特性:值仅为true/false
# bool 变量:true或者false
# @export导出变量;is_game_over标记游戏是否结束;:bool指定布尔类型;=false初始值为游戏未结束
@export var is_game_over : bool = false
# @export导出变量;bullet_scene为自定义变量名;:PackedScene指定类型为打包场景,用于加载子弹预制体(需在编辑器绑定子弹场景文件)
@export var bullet_scene : PackedScene
# 定义Godot内置物理帧回调函数,固定60次/秒执行(适配物理/移动逻辑,不受渲染帧率影响)
# func声明函数;_physics_process为物理帧回调名;(delta:float)接收帧间隔时间参数;-> void表示无返回值
func _physics_process(delta:float) -> void:
# 条件判断:仅当游戏未结束(is_game_over为false)时,执行移动和动画逻辑
if not is_game_over:
# 计算角色移动速度向量:通过Input.get_vector获取上下左右按键输入,返回归一化方向向量(避免斜向速度翻倍),乘以move_speed得到最终速度,赋值给velocity(CharacterBody2D内置速度属性)
velocity= Input.get_vector("left","right","up","down")*move_speed
# 执行角色移动:CharacterBody2D核心方法,根据velocity移动,自动处理碰撞、地面检测等物理逻辑
move_and_slide()
# 注释:如果速度为0,播放待机动画
# 条件判断:velocity为零向量(Vector2.ZERO)表示角色静止
if velocity == Vector2.ZERO:
# 调用动画精灵的play方法,播放名为'idle'的待机动画(需提前在AnimatedSprite2D中配置该动画)
animator.play('idle')
# 注释:如果速度不为0,播放跑步动画
# 否则(角色有移动速度)
else:
# 播放名为'run'的跑步动画(需提前配置该动画)
animator.play('run')
# 注:此处重复调用move_and_slide()属于冗余代码,会导致角色移动两次(速度异常),建议删除此行
move_and_slide()
# 自定义游戏结束函数,无参数、无返回值,处理游戏结束逻辑
func game_over():
# 将游戏结束标记设为true,后续_physics_process中的移动/动画逻辑会停止执行
is_game_over = true
# 播放名为"game_over"的结束动画(需提前配置该动画)
animator.play("game_over")
# 等待3秒:创建3秒定时器,await暂停函数执行,直到定时器触发timeout信号(3秒后继续)
await get_tree().create_timer(3).timeout
# 重新加载当前场景:调用场景树方法,实现游戏重启
get_tree().reload_current_scene()
# 自定义开火/发射子弹函数,无参数、无返回值,处理子弹发射逻辑
func _on_fire():
# 条件判断:如果角色处于移动状态(速度非零)或游戏已结束,直接退出函数(不发射子弹)
if velocity !=Vector2.ZERO or is_game_over:
# return:终止函数执行,后续发射子弹的代码不再运行
return
# 实例化子弹场景:调用PackedScene的instantiate方法,创建子弹节点实例
var bullet_node = bullet_scene.instantiate()
# 设置子弹初始位置:在角色位置基础上偏移(Vector2(6,6)),避免子弹和角色重叠
bullet_node.position = position + Vector2(6,6)
# 将子弹添加到当前场景根节点:使子弹在场景中显示并执行自身逻辑
get_tree().current_scene.add_child(bullet_node)冗余代码:
_physics_process中两次调用move_and_slide(),会导致角色移动速度翻倍、物理逻辑异常,需删除其中一行;变量权限:
is_game_over无需加@export(游戏结束状态是内部逻辑,无需在编辑器手动调整),建议移除@export避免误改;开火逻辑:
_on_fire()仅在角色静止且游戏未结束时触发,符合“静止才能发射”的设计,但需给该函数绑定触发方式(如按键):# 可在_physics_process函数内添加(放在if not is_game_over判断内) if Input.is_action_just_pressed("fire"): # "fire"需在输入映射中配置(如空格、鼠标左键) _on_fire()动画依赖:
idle/run/game_over动画名需与AnimatedSprite2D节点中配置的动画名称完全一致,否则动画播放失败;子弹场景:
bullet_scene需在编辑器中绑定子弹场景文件,且子弹场景根节点需为Node2D子类(如Area2D),否则无法设置position。
sline_2d
以下是整合所有代码到一个代码块中,每行下方附带详细注释的版本,清晰解析语法含义、功能逻辑和关键注意事项:
# 继承Godot内置的Area2D节点,Area2D是2D碰撞检测节点(无物理体,仅用于检测碰撞/重叠),适合做子弹这类需要碰撞触发逻辑的节点
extends Area2D
# @export:导出变量,可在编辑器检查器面板可视化调整数值;var声明变量;bullet_speed是自定义变量名(子弹移动速度);:float指定浮点型;=100设置默认速度(像素/秒,正数表示X轴向右移动)
@export var bullet_speed : float = 100
# 注释:节点首次进入场景树时调用(初始化逻辑)
# Called when the node enters the scene tree for the first time.
# func声明函数;_ready是Godot内置回调函数,节点加载完成后仅执行一次;无参数、无返回值
func _ready():
# 等待3秒:创建一个3秒的定时器,await暂停函数执行,直到定时器完成(3秒后继续执行后续代码)
await get_tree().create_timer(3)
# 销毁子弹节点:queue_free()是Node的内置方法,将节点标记为待销毁,避免子弹一直存在占用内存
queue_free()
# 注释:每帧调用,delta是上一帧到当前帧的时间差
# Called every frame. 'delta' is the elapsed time since the previous frame.
# func声明函数;_physics_process是物理帧回调函数(固定60次/秒执行,适配物理/移动逻辑);(delta: float)接收帧间隔时间参数;-> void表示无返回值
func _physics_process(delta: float) -> void:
# 控制子弹移动:
# position是Node2D的内置属性(Vector2类型),表示节点在场景中的坐标;
# += 赋值运算符,将原有坐标加上新的位移量;
# Vector2(bullet_speed,0):X轴方向以bullet_speed速度移动,Y轴保持不动;
# * delta:乘以帧间隔时间,消除帧率差异(保证不同设备/帧率下子弹移动速度一致);
# 最终效果:子弹以每秒100像素的速度持续向右移动
position += Vector2(bullet_speed,0) * delta核心细节与优化提醒:
缩进规范:若之前报「Mixed use of tabs and spaces」错误,需确保所有缩进统一为4个空格(GDScript官方推荐),可通过Godot编辑器「编辑→转换缩进→转换为空格」批量修正;
定时器优化:
_ready()中await get_tree().create_timer(3)可补充timeout信号(写法更规范),效果一致:await get_tree().create_timer(3).timeout碰撞逻辑补充:子弹是Area2D节点,若需检测击中目标(如敌人),需添加
body_entered信号回调(参考之前史莱姆的碰撞逻辑):# 可新增函数,需在编辑器绑定Area2D的body_entered信号 func _on_body_entered(body: Node2D) -> void: if body is Area2D and body.name == "Enemy": # 假设敌人节点名是Enemy body.queue_free() # 销毁敌人 queue_free() # 销毁子弹方向控制:若需要子弹向左移动,只需将
bullet_speed设为负数(如-100),无需修改移动逻辑;内存管理:
queue_free()是安全的销毁方式,避免直接用free()导致的内存泄漏问题,3秒自动销毁能防止子弹越界后一直占用资源。
分组功能
注意事项
#
#@export var bullet_speed : float = 100
#
## Called when the node enters the scene tree for the first time.
#func _ready():
#await get_tree().create_timer(3)
#queue_free()
#
#
## Called every frame. 'delta' is the elapsed time since the previous frame.
#func _physics_process(delta: float) -> void:
#position += Vector2(bullet_speed,0) * delta
extends Area2D
@export var bullet_speed : float = 100 # 正数是向右发射
func _ready():
await get_tree().create_timer(3).timeout # 3秒后销毁
queue_free()
func _physics_process(delta: float) -> void:
position += Vector2(bullet_speed, 0) * delta # 向右移动
你的代码和我提供的代码核心逻辑一致,但缩进格式、语法细节、注释符号这三个关键问题导致运行失败,以下是逐点拆解:
问题1:缩进混用「Tab/空格」(最核心原因)
GDScript对缩进要求极其严格,你的代码中:
func _ready():
#await get_tree().create_timer(3) # 这里用了「Tab」缩进
#queue_free()
func _physics_process(delta: float) -> void:
#position += Vector2(bullet_speed,0) * delta # 同样是Tab缩进而我提供的代码是统一用4个空格缩进(Godot官方推荐),混合Tab/空格会触发「Mixed use of tabs and spaces」语法错误,直接导致脚本无法执行,子弹节点加载后既不移动也不销毁。
问题2:_ready()里的定时器缺少.timeout(语法不完整)
你的代码:
await get_tree().create_timer(3) # 缺少.timeout信号正确写法:
await get_tree().create_timer(3).timeout # 必须加.timeoutcreate_timer(3)只是创建定时器,await需要等待定时器的timeout信号才会暂停执行,缺少这个信号会导致await无效,子弹会立刻执行queue_free(),刚生成就被销毁,肉眼看不到发射效果。
问题3:注释符号与代码激活状态(次要但影响测试)
你的代码全部被#注释(开头的#extends Area2D),如果是误注释,脚本会完全失效(相当于空脚本);如果是你测试时注释的,取消注释后才会生效。
问题4:函数名后多余空格(语法规范问题)
你的代码:
func _ready(): # _ready前有2个空格正确写法:
func _ready(): # 函数名和括号之间无空格虽然Godot对这种空格兼容性较好,但可能触发语法警告,极端情况下也会影响脚本执行。
修正后的完整代码(直接可用)
extends Area2D
@export var bullet_speed : float = 100
# Called when the node enters the scene tree for the first time.
func _ready():
await get_tree().create_timer(3).timeout # 补全.timeout
queue_free()
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(delta: float) -> void:
position += Vector2(bullet_speed,0) * delta快速验证方法
全选你的代码,删除后粘贴上面的修正版;
在Godot编辑器中点击「编辑→转换缩进→转换为空格」,确保缩进统一;
运行游戏,按开火键,子弹会正常发射并向右移动,3秒后自动销毁。
如果仍看不到子弹,补充检查:
子弹节点的
CollisionShape2D是否配置(无碰撞形状不影响移动,但可能看不到可视化效果);子弹的
Z Index是否高于背景/玩家(避免被遮挡);bullet_speed是否设为正数(向右发射),如果你的游戏是左向移动,设为负数(如-100)。
随机生成史莱姆
新建个脚本GameManger.gd

# 继承Node2D节点,作为生成史莱姆的管理器节点
extends Node2D
# 导出变量:用于在编辑器中指定史莱姆的预制体(PackedScene类型)
@export var slime_scene : PackedScene
# 导出变量:用于在编辑器中关联控制生成间隔的定时器节点
@export var spawn_timer : Timer
# Called when the node enters the scene tree for the first time.
# _ready函数:节点首次进入场景树时调用(初始化逻辑)
func _ready():
# 暂时留空,可后续添加初始化逻辑(比如绑定定时器信号)
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
# _process函数:每帧调用,delta是从上一帧到当前帧的耗时(秒)
func _process(delta):
# 每帧减少定时器的等待时间:每秒减少0.2秒(0.2 * delta 是按帧时间比例减少)
spawn_timer.wait_time -= 0.2 * delta
# 限制等待时间范围:最小1秒,最大3秒(防止时间过小/过大)
spawn_timer.wait_time = clamp(spawn_timer.wait_time,1,3)
# 自定义函数:生成史莱姆的核心逻辑
func _spawn_slime():
# 从预制体实例化一个史莱姆节点
var slime_node = slime_scene.instantiate()
# 设置史莱姆生成位置:x固定260,y在50到115之间随机
slime_node.position = Vector2(260,randf_range(50,115))
# 将生成的史莱姆节点添加到当前场景的根节点下(使其显示在场景中)
get_tree().current_scene.add_child(slime_node)核心逻辑解释
_ready()中绑定信号 + 启动定时器:是定时器生效的前提;_process()中动态修改等待时间:实现 “生成速度越来越快(直到 1 秒 / 个)” 的效果;_on_spawn_timer_timeout()回调:定时器到点后生成史莱姆并重启定时器,实现循环生成;_spawn_slime():负责具体的史莱姆实例化、位置设置、添加到场景。
如果只需要对原始代码逐行加注释,第一个代码块即可;如果需要让代码实际能运行,第二个完整版本更合适。

场景选择Sline(史莱姆)
补丁
史莱姆变多,如果超出场景,史莱姆将自动销毁,我们需要在sline_2d中添加以下代码
if position.x < -267:
queue_free()11-12行
UI
.current_scene:场景树的属性,指向当前加载并显示的「主场景根节点」(比如你运行游戏时选择的那个场景文件)。
添加文字和分数


修改了GameManger.gd
extends Node2D
@export var slime_scene : PackedScene # 场景
@export var spawn_timer : Timer # 时间
@export var score : int = 0 # 分数
@export var score_label : Label
@export var game_over_label:Label
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
spawn_timer.wait_time -= 0.2 * delta
spawn_timer.wait_time = clamp(spawn_timer.wait_time,1,3)
score_label.text = "Score:"+str(score)
func _spawn_slime():
var slime_node = slime_scene.instantiate()
slime_node.position = Vector2(260,randf_range(50,115))
get_tree().current_scene.add_child(slime_node)
func show_game_over():
game_over_label.visible = true
添加4个全局变量

还修改了Player.gd
extends CharacterBody2D
@export var move_speed : float = 50
@export var animator : AnimatedSprite2D
# bool 变量:true或者false
@export var is_game_over : bool = false
@export var bullet_scene : PackedScene
func _physics_process(delta:float) -> void:
if not is_game_over:
velocity= Input.get_vector("left","right","up","down")*move_speed
# 如果速度为0,播放待机动画
if velocity == Vector2.ZERO:
animator.play('idle')
# 如果速度不为0,播放跑不动画
else:
animator.play('run')
move_and_slide()
func game_over():
if not is_game_over:
is_game_over = true
animator.play("game_over")
get_tree().current_scene.show_game_over()
await get_tree().create_timer(3).timeout
get_tree().reload_current_scene()
func _on_fire():
#if velocity !=Vector2.ZERO or is_game_over:
#return
var bullet_node = bullet_scene.instantiate()
bullet_node.position = position + Vector2(6,6)
get_tree().current_scene.add_child(bullet_node)
添加了一行代码sline_2d
get_tree().current_scene.score +=1在24-26之间代码
音乐音效

sline_2d.gd
func _on_area_entered(area: Area2D) -> void:
# 检测撞到史莱姆的是否是子弹
if area.is_in_group("bullet"):
# 播放被消灭的动画、消除子弹
$AnimatedSprite2D.play("death")
is_dead = true
area.queue_free()
get_tree().current_scene.score +=1
$DeathSound.play()
# 延迟0.6秒后移除史莱姆节点
await get_tree().create_timer(0.6).timeout
queue_free()
Player.gd


func _process(delta):
if velocity == Vector2.ZERO or is_game_over:
$RunningSound.stop()
elif not $RunningSound.playing:
$RunningSound.play()func game_over():
if not is_game_over:
is_game_over = true
animator.play("game_over")
get_tree().current_scene.show_game_over()
$GameOverSound.play()
await get_tree().create_timer(3).timeout
get_tree().reload_current_scene()
func _on_fire():
#如果玩家在移动,或者游戏结束了,则不生成子弹
#if velocity !=Vector2.ZERO or is_game_over:
#return
$FireSound.play()
var bullet_node = bullet_scene.instantiate()
bullet_node.position = position + Vector2(6,6)
get_tree().current_scene.add_child(bullet_node)完整代码
玩家代码palyer.gd
extends CharacterBody2D
@export var move_speed : float = 50
@export var animator : AnimatedSprite2D
# bool 变量:true或者false
@export var is_game_over : bool = false
@export var bullet_scene : PackedScene
func _process(delta):
if velocity == Vector2.ZERO or is_game_over:
$RunningSound.stop()
elif not $RunningSound.playing:
$RunningSound.play()
func _physics_process(delta:float) -> void:
if not is_game_over:
velocity= Input.get_vector("left","right","up","down")*move_speed
# 如果速度为0,播放待机动画
if velocity == Vector2.ZERO:
animator.play('idle')
# 如果速度不为0,播放跑不动画
else:
animator.play('run')
move_and_slide()
func game_over():
if not is_game_over:
is_game_over = true
animator.play("game_over")
get_tree().current_scene.show_game_over()
$GameOverSound.play()
$RestarTimer.start()
func _on_fire():
#如果玩家在移动,或者游戏结束了,则不生成子弹
#if velocity !=Vector2.ZERO or is_game_over:
#return
$FireSound.play()
var bullet_node = bullet_scene.instantiate()
bullet_node.position = position + Vector2(6,6)
get_tree().current_scene.add_child(bullet_node)
func _reload_scene():
get_tree().reload_current_scene()
这份代码是 Godot 4.x 中玩家角色(CharacterBody2D 节点)的核心逻辑,包含移动、动画、音效、游戏结束、子弹发射、场景重启等功能,以下逐模块解析(保持代码原有结构,标注核心逻辑和细节):
一、代码头部:基础声明与变量定义
extends CharacterBody2D # 玩家节点继承自CharacterBody2D(2D物理运动节点,用于带碰撞的移动)
# 导出变量(可在编辑器可视化调整,无需改代码)
@export var move_speed : float = 50 # 移动速度(像素/秒)
@export var animator : AnimatedSprite2D # 动画播放器节点(绑定AnimatedSprite2D子节点)
@export var is_game_over : bool = false # 游戏结束标记(true=结束,false=正常)
@export var bullet_scene : PackedScene # 子弹预制体(需在编辑器拖入子弹场景文件)extends CharacterBody2D:决定了玩家可以使用move_and_slide()(带碰撞的移动)、velocity(速度向量)等物理运动相关的核心方法/属性;@export关键字:让变量出现在编辑器的「检查器」面板,方便调试(比如调整移动速度、更换子弹预制体);变量类型标注(
: float/: bool):GDScript 是动态类型语言,但标注类型可减少报错,提升可读性。
二、_process(delta):每帧执行的非物理逻辑(音效控制)
func _process(delta): # delta=帧间隔时间(秒),用于统一不同帧率下的逻辑速度
# 条件:玩家静止(速度为0) 或 游戏结束 → 停止移动音效
if velocity == Vector2.ZERO or is_game_over:
$RunningSound.stop() # 访问子节点RunningSound(音频节点),调用stop()停止播放
# 条件:玩家非静止 且 移动音效未播放 → 播放移动音效
elif not $RunningSound.playing:
$RunningSound.play()核心作用:根据玩家状态控制移动音效的播放/停止;
关键细节:
RunningSound:Godot 中 是「子节点路径简写」,代表访问当前节点下名为
RunningSound的子节点(需是AudioStreamPlayer/AudioStreamPlayer2D音频节点);velocity == Vector2.ZERO:velocity是CharacterBody2D的核心属性(速度向量),Vector2.ZERO代表 (0,0),即玩家静止;$RunningSound.playing:判断音频是否正在播放,避免重复调用play()导致音效卡顿。
三、_physics_process(delta):物理帧执行的核心逻辑(移动+动画)
func _physics_process(delta:float) -> void: # 物理帧回调(帧率固定,适合物理运动)
# 只有游戏未结束时,才执行移动逻辑
if not is_game_over:
# 1. 获取方向输入,计算速度向量
# Input.get_vector("left","right","up","down"):根据输入映射返回(-1/0/1, -1/0/1)的方向向量
# 乘以move_speed后,得到最终移动速度(像素/物理帧)
velocity= Input.get_vector("left","right","up","down")*move_speed
# 2. 根据速度控制动画播放
if velocity == Vector2.ZERO: # 静止 → 播放待机动画
animator.play('idle') # animator是绑定的AnimatedSprite2D,播放名为idle的动画
else: # 移动中 → 播放跑步动画
animator.play('run')
# 3. 执行带碰撞的移动(CharacterBody2D核心方法,自动处理碰撞回弹)
move_and_slide()核心作用:游戏未结束时,处理玩家的方向移动和动画切换;
关键细节:
Input.get_vector():依赖「项目设置→输入映射」中配置的left/right/up/down动作(比如left绑定A键、right绑定D键);animator.play('idle'):需确保AnimatedSprite2D节点中已创建名为idle/run的动画帧(否则会报错);move_and_slide():自动应用velocity移动玩家,且会检测碰撞(比如墙、障碍物),是CharacterBody2D最常用的移动方法。
四、game_over():游戏结束逻辑
func game_over():
# 防止重复调用(游戏结束后不再执行)
if not is_game_over:
is_game_over = true # 标记游戏结束
animator.play("game_over") # 播放游戏结束动画
# 调用当前场景根节点的show_game_over()方法(需场景根节点实现该方法,比如显示结束UI)
get_tree().current_scene.show_game_over()
$GameOverSound.play() # 播放游戏结束音效
$RestarTimer.start() # 启动重启定时器(需子节点RestarTimer是Timer节点)核心作用:触发游戏结束的一系列行为;
关键细节:
if not is_game_over:避免多次调用(比如玩家多次碰撞敌人时,只执行一次结束逻辑);get_tree().current_scene.show_game_over():要求当前场景的根节点脚本中定义了show_game_over()函数(比如显示“游戏结束”文本、按钮);$RestarTimer.start():启动重启定时器(Timer节点),定时器超时后应调用_reload_scene()重启场景(需在编辑器中绑定定时器的timeout信号到_reload_scene)。
五、_on_fire():子弹发射逻辑
func _on_fire():
# 注释掉的逻辑:原本想限制“移动时不发射”,现在已取消
#if velocity !=Vector2.ZERO or is_game_over:
#return
$FireSound.play() # 播放射击音效
# 1. 实例化子弹节点(从预制体创建)
var bullet_node = bullet_scene.instantiate()
# 2. 设置子弹生成位置:玩家位置偏移(6,6)(避免子弹和玩家重叠)
bullet_node.position = position + Vector2(6,6)
# 3. 将子弹添加到当前场景(否则子弹不会显示/运行)
get_tree().current_scene.add_child(bullet_node)核心作用:生成子弹并添加到场景;
关键细节:
bullet_scene.instantiate():从导出的子弹预制体创建实例(需在编辑器绑定子弹的.tscn文件);position + Vector2(6,6):玩家的position是锚点(通常是中心),偏移后子弹从玩家“枪口”位置生成;get_tree().current_scene.add_child(bullet_node):将子弹添加到场景根节点(而非玩家子节点),避免玩家移动时子弹跟着移动。
六、_reload_scene():场景重启逻辑
func _reload_scene():
get_tree().reload_current_scene() # 重新加载当前运行的场景(重置游戏)核心作用:重启游戏场景;
触发方式:需在编辑器中给
RestarTimer节点的timeout信号绑定到该函数(游戏结束后,定时器倒计时结束自动重启)。
七、代码整体逻辑梳理
正常游戏状态:
玩家通过方向键/WSAD移动,
_physics_process处理移动和动画;移动时播放跑步音效,静止时停止;
调用
_on_fire()时生成子弹(需绑定输入/信号触发,比如J键)。
游戏结束状态:
调用
game_over()后,is_game_over设为true,停止移动逻辑;播放结束动画、音效,显示结束UI,启动重启定时器;
定时器超时后调用
_reload_scene()重启场景。
八、代码待完善/注意事项
_on_fire()未绑定触发逻辑:当前代码中_on_fire()只是定义了,但没有绑定到J键输入/按钮点击,需补充输入监听(比如之前提到的J键按下触发);缺少空值检查:比如
bullet_scene未绑定预制体时,instantiate()会报错,需加if bullet_scene:校验;子弹生成位置固定:
Vector2(6,6)是硬编码,可改为导出变量方便调整;输入映射依赖:
left/right/up/down需在「项目设置→输入映射」中配置,否则无法移动。
这份代码是典型的2D俯视角/横版玩家核心逻辑,结构清晰,覆盖了移动、动画、音效、游戏流程的核心需求,仅需补充射击触发逻辑即可完整运行。
如果选择按下J射击游戏代码如下
extends CharacterBody2D # 玩家节点继承CharacterBody2D(2D物理运动节点,支持带碰撞的移动)
# 导出变量(编辑器可视化配置,无需修改代码)
@export var move_speed : float = 50 # 玩家移动速度(像素/秒)
@export var animator : AnimatedSprite2D # 绑定动画播放器节点(控制idle/run/game_over动画)
@export var is_game_over : bool = false # 游戏结束标记(true=结束,false=正常运行)
@export var bullet_scene : PackedScene # 子弹预制体(需在编辑器拖入子弹场景文件)
@export var fire_interval: float = 0.1 # 子弹发射间隔(秒),越小射速越快
@export var fire_offset: Vector2 = Vector2(6, 6) # 子弹生成偏移(避免和玩家位置重叠)
# 内部变量(仅代码内使用,编辑器不可见)
var fire_timer: Timer # 控制连续射击的定时器(避免按住J键帧级发射子弹)
var is_firing: bool = false # 射击状态标记(true=正在射击,false=停止射击)
func _ready(): # 节点首次进入场景树时执行(初始化逻辑)
# 初始化射击定时器(代码创建,无需手动在场景添加Timer节点)
fire_timer = Timer.new()
fire_timer.name = "FireTimer" # 给定时器命名,方便调试
fire_timer.wait_time = fire_interval # 设置定时器超时时间(即发射间隔)
fire_timer.one_shot = false # 设为循环模式(超时后自动重启,实现连续发射)
fire_timer.timeout.connect(_on_fire) # 绑定定时器超时信号到发射子弹函数
add_child(fire_timer) # 将定时器添加到场景树(否则无法生效)
fire_timer.stop() # 强制定时器初始状态为停止(避免自动发射)
print("初始化完成,定时器是否停止:", fire_timer.is_stopped()) # 调试打印,确认定时器初始状态
func _process(delta): # 每帧执行(非物理逻辑:音效+射击输入监听)
# 原有移动音效控制逻辑:静止/游戏结束时停止跑步音效,移动时播放
if velocity == Vector2.ZERO or is_game_over:
$RunningSound.stop() # 停止子节点RunningSound(音频节点)的播放
elif not $RunningSound.playing: # 音效未播放时才调用play,避免重复播放卡顿
$RunningSound.play()
# 射击输入监听:仅游戏未结束时响应J键操作
if not is_game_over:
# 检测J键按下瞬间(Input映射中"fire"绑定J键)
if Input.is_action_just_pressed("fire"):
is_firing = true # 标记为正在射击
fire_timer.start() # 启动定时器,开始循环发射
print("[按下J] 开始射击,定时器运行中:", not fire_timer.is_stopped()) # 调试打印射击状态
# 检测J键松开瞬间
elif Input.is_action_just_released("fire"):
is_firing = false # 标记为停止射击
fire_timer.stop() # 停止定时器,结束连续发射
print("[松开J] 停止射击,定时器运行中:", not fire_timer.is_stopped()) # 调试打印停止状态
else:
# 游戏结束时强制停止射击(双重保险)
is_firing = false
fire_timer.stop()
func _physics_process(delta:float) -> void: # 物理帧执行(固定帧率,适合物理移动)
if not is_game_over: # 游戏未结束时才处理移动
# 根据方向输入计算速度向量(Input映射中"left/right/up/down"绑定对应按键)
velocity = Input.get_vector("left","right","up","down") * move_speed
# 根据速度控制动画:静止播放idle,移动播放run
if velocity == Vector2.ZERO:
animator.play('idle')
else:
animator.play('run')
move_and_slide() # CharacterBody2D核心方法:带碰撞的移动(自动处理碰撞回弹)
func game_over(): # 游戏结束触发函数(通常由史莱姆碰撞调用)
if not is_game_over: # 避免重复调用
is_game_over = true # 标记游戏结束
animator.play("game_over") # 播放游戏结束动画
get_tree().current_scene.show_game_over() # 调用场景根节点的结束UI显示函数
$GameOverSound.play() # 播放游戏结束音效
$RestarTimer.start() # 启动重启定时器(超时后调用_reload_scene)
# 游戏结束时强制停止射击
fire_timer.stop()
is_firing = false
# 发射子弹核心逻辑(由定时器超时信号触发)
func _on_fire():
# 三重校验:防止无效发射(射击状态/定时器状态/游戏状态/子弹预制体)
if not is_firing or fire_timer.is_stopped() or is_game_over or not bullet_scene:
# 调试打印跳过发射的原因,方便定位问题
print("[跳过发射] 原因:射击状态=", is_firing, " 定时器停止=", fire_timer.is_stopped(), " 游戏结束=", is_game_over)
return # 校验不通过则退出函数,不生成子弹
# 播放射击音效(防空检查:避免FireSound节点不存在导致报错)
if $FireSound:
$FireSound.play()
# 生成子弹:从预制体实例化→设置位置→添加到场景
var bullet_node = bullet_scene.instantiate()
bullet_node.position = position + fire_offset # 玩家位置+偏移量,避免子弹和玩家重叠
get_tree().current_scene.add_child(bullet_node) # 子弹添加到场景根节点(而非玩家子节点)
print("[发射子弹] 成功生成") # 调试打印,确认子弹生成
func _reload_scene(): # 场景重启函数(由RestarTimer超时信号触发)
get_tree().reload_current_scene() # 重新加载当前场景,实现游戏重启核心逻辑总结(补充说明)
射击机制核心:用
Timer控制发射间隔(避免按住 J 键帧级发射),Input.is_action_just_pressed/released监听 J 键的「按下 / 松开瞬间」,分别启动 / 停止定时器,实现「按住 J 连续发射、松开停止」的效果;状态校验:
_on_fire中多重校验(射击状态 / 定时器状态 / 游戏状态),避免无效发射(比如游戏结束后仍发射、定时器意外启动等);兼容性优化:移除了 Timer 的
stopped信号绑定(避免版本兼容问题),改用is_stopped()方法判断定时器状态,手动同步is_firing标记;调试友好:关键节点添加
print语句,可在输出面板查看射击状态、子弹生成、跳过发射的原因,快速定位问题。
整体代码保留了原有的移动、动画、游戏结束逻辑,新增了可控的连续射击功能,且通过多重校验和调试打印提升了代码的健壮性。
子弹代码bullet_2d.gd
extends Area2D # 子弹节点继承自Area2D(2D区域节点,核心用于碰撞检测,无物理体但可检测进入/离开)
@export var bullet_speed : float = 100 # 导出变量:子弹移动速度(像素/秒),正数向右、负数向左
# 可在编辑器直接调整,无需修改代码
func _ready(): # 子弹节点首次进入场景树时执行(初始化逻辑)
# 核心逻辑:等待3秒后销毁子弹(避免子弹无限存在占用内存)
# await:暂停函数执行,直到定时器超时;create_timer(3)创建一个3秒的临时定时器
# timeout:定时器的超时信号,触发后执行后续代码
await get_tree().create_timer(3).timeout
queue_free() # 销毁当前子弹节点(从场景树中移除并释放内存)
func _physics_process(delta: float) -> void: # 物理帧执行(固定帧率,适合运动逻辑)
# 子弹移动逻辑:每物理帧沿X轴向右移动(Vector2(x方向, y方向))
# Vector2(bullet_speed, 0):Y轴速度为0,仅X轴移动;乘以delta统一不同帧率下的移动速度
position += Vector2(bullet_speed, 0) * delta # position是节点的2D坐标,直接修改实现移动核心逻辑与细节补充
1. 节点类型选择(Area2D)
Area2D是无物理碰撞体的「区域节点」,适合子弹这类需要碰撞检测(击中敌人/障碍物) 但不需要物理受力(比如被推开)的对象;对比
CharacterBody2D(玩家用的物理移动节点):子弹无需复杂的物理移动(如碰撞回弹),用Area2D更轻量,仅需简单的坐标偏移即可实现移动。
2. 关键方法解析
3. 代码特点与待优化点
优点:
① 轻量简洁:核心移动+自动销毁逻辑仅几行,无冗余代码;
② 可配置性:bullet_speed 导出后可在编辑器可视化调整,适配不同射速需求;
③ 内存安全:3秒自动销毁,避免子弹无限累积导致游戏卡顿。
待优化点:
① 无碰撞检测:当前子弹击中史莱姆/边界后不会销毁,需补充 _on_body_entered 或 _on_area_entered 碰撞回调;
② 移动方向单一:仅支持X轴向右,可扩展为根据发射方向动态调整Vector2的X/Y值;
③ 无边界检测:子弹超出屏幕后仍会移动3秒才销毁,可添加屏幕边界检测提前销毁。
4. 扩展示例(补充碰撞销毁逻辑)
如果需要子弹击中史莱姆后立即销毁,可添加以下代码:
extends Area2D
@export var bullet_speed : float = 100
func _ready():
await get_tree().create_timer(3).timeout
queue_free()
# 绑定碰撞回调(需确保Area2D勾选了"Monitorable",史莱姆节点勾选了"Collision Layer")
body_entered.connect(_on_body_hit) # 检测击中物理体(如CharacterBody2D)
# area_entered.connect(_on_area_hit) # 可选:检测击中其他Area2D
func _physics_process(delta: float) -> void:
position += Vector2(bullet_speed, 0) * delta
# 击中物理体时触发(比如史莱姆)
func _on_body_hit(hit_body):
# 检测击中的是否是史莱姆(需给史莱姆节点添加"slime"分组)
if hit_body.is_in_group("slime"):
queue_free() # 立即销毁子弹整体来说,这份代码是子弹最基础的「移动+自动销毁」逻辑,满足核心功能且易于扩展,是2D游戏中子弹的典型基础实现。
主场景代码GameManger.gd
extends Node2D # 场景根节点继承Node2D(2D节点的基础类型,无物理特性,适合作为管理器/容器)
# 导出变量(编辑器可视化配置,无需改代码)
@export var slime_scene : PackedScene # 史莱姆预制体(需在编辑器拖入史莱姆场景文件,用于生成敌人)
@export var spawn_timer : Timer # 控制史莱姆生成间隔的定时器节点(需在编辑器绑定场景中的Timer节点)
@export var score : int = 0 # 游戏分数(初始值0,击中史莱姆时累加)
@export var score_label : Label # 分数显示标签(绑定UI的Label节点,用于实时显示分数)
@export var game_over_label:Label # 游戏结束提示标签(绑定UI的Label节点,游戏结束时显示)
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body. # 节点首次进入场景时执行,当前留空(可后续添加初始化逻辑,比如启动定时器)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
# 核心逻辑1:动态减少史莱姆生成间隔(实现“难度递增”)
# 每帧减少定时器的等待时间:0.2 * delta 表示“每秒减少0.2秒”(delta是帧间隔,保证不同帧率下速率一致)
spawn_timer.wait_time -= 0.2 * delta
# 限制等待时间范围:最小1秒、最大3秒(防止生成间隔过小/过大,控制难度上限)
spawn_timer.wait_time = clamp(spawn_timer.wait_time,1,3)
# 核心逻辑2:实时更新分数显示
# str(score):将整数分数转为字符串,拼接成"Score:XX"格式,赋值给Label的text属性显示
score_label.text = "Score:"+str(score)
# 自定义函数:生成史莱姆的核心逻辑
func _spawn_slime():
# 1. 从史莱姆预制体实例化一个史莱姆节点
var slime_node = slime_scene.instantiate()
# 2. 设置史莱姆生成位置:X轴固定260(屏幕右侧),Y轴在50~115之间随机(垂直方向分散生成)
slime_node.position = Vector2(260,randf_range(50,115))
# 3. 将史莱姆节点添加到当前场景根节点(使其显示在场景中,而非当前管理器节点下)
get_tree().current_scene.add_child(slime_node)
# 自定义函数:显示游戏结束提示(由玩家的game_over()函数调用)
func show_game_over():
# 将游戏结束标签设为可见(需确保该Label初始状态visible=false)
game_over_label.visible = true核心逻辑与细节补充
1. 节点定位:场景全局管理器
该脚本绑定在场景根节点(Node2D) 上,是整个游戏的「全局管理器」,负责三大核心功能:
控制史莱姆生成频率(难度递增);
管理游戏分数并实时显示;
处理游戏结束的UI提示。
2. 关键代码解析
3. 代码待完善/关键缺失逻辑
该代码是“半成品”,缺少以下核心逻辑才能正常运行:
(1)史莱姆自动生成(定时器信号绑定)
当前仅定义了_spawn_slime(),但未绑定到定时器的超时信号,需补充:
func _ready():
# 绑定定时器超时信号到生成函数(定时器到点后自动生成史莱姆)
spawn_timer.timeout.connect(_spawn_slime)
# 启动定时器(开始首次生成倒计时)
spawn_timer.start()(2)分数累加逻辑
当前仅定义了score变量,但未在“击中史莱姆”时累加,需在史莱姆脚本中补充:
# 史莱姆脚本中击中子弹的回调里添加
get_tree().current_scene.score += 1 # 每击中一个史莱姆,分数+1(3)空值校验(防错)
未对导出变量做空值检查,比如spawn_timer未绑定会导致_process中报错,需补充:
func _process(delta):
if spawn_timer: # 先检查定时器是否有效
spawn_timer.wait_time -= 0.2 * delta
spawn_timer.wait_time = clamp(spawn_timer.wait_time,1,3)
if score_label: # 检查分数标签是否有效
score_label.text = "Score:"+str(score)4. 整体运行流程(补充后)
游戏启动 →
_ready()启动定时器,绑定生成函数;每帧执行
_process():<br>→ 减少定时器间隔(难度递增)→ 实时更新分数显示;定时器超时 → 调用
_spawn_slime()生成史莱姆;玩家击中史莱姆 → 场景根节点的
score+1 → 分数标签实时更新;玩家碰撞史莱姆 → 调用
show_game_over()→ 显示结束标签,游戏结束。
这份代码是2D闯关游戏的「核心管理器」骨架,结构清晰但需补充信号绑定、分数累加等逻辑才能完整运行,核心设计思路是“动态调整生成频率实现难度递增”,符合经典休闲游戏的难度设计逻辑。
史莱姆代码sline_2d.gd
extends Area2D # 史莱姆节点继承Area2D(2D区域节点,核心用于碰撞检测:检测与玩家/子弹的接触)
@export var slime_speed : float = -100 # 导出变量:史莱姆移动速度(像素/秒)
# 负数表示沿X轴向左移动(对应从屏幕右侧向左侧移动)
# 可在编辑器直接调整速度,无需改代码
var is_dead : bool =false # 内部状态标记:是否已被消灭(true=死亡,false=存活)
# 用于控制移动、碰撞逻辑(死亡后不再响应)
func _physics_process(delta: float) -> void: # 物理帧执行(固定帧率,适合运动逻辑)
# 核心逻辑1:仅存活状态下移动
if not is_dead:
# 沿X轴向左移动(Vector2(x方向, y方向),Y轴速度为0)
# 乘以delta保证不同帧率下移动速度一致
position += Vector2(slime_speed, 0) * delta
# 核心逻辑2:超出屏幕左侧边界后自动销毁(避免内存泄漏)
if position.x < -267:
queue_free() # 安全销毁节点(从场景树移除并释放内存)
func _on_body_entered(body:Node2D) -> void: # 碰撞回调:检测到物理体(如CharacterBody2D)进入区域时触发
# 条件:碰撞的是玩家(CharacterBody2D)且史莱姆存活
if body is CharacterBody2D and not is_dead:
body.game_over() # 调用玩家节点的game_over()函数,触发游戏结束逻辑
func _on_area_entered(area: Area2D) -> void: # 碰撞回调:检测到其他Area2D(如子弹)进入区域时触发
# 条件:碰撞的是子弹(需给子弹节点添加"bullet"分组)
if area.is_in_group("bullet"):
# 1. 播放死亡动画(绑定的AnimatedSprite2D子节点,需提前创建"death"动画帧)
$AnimatedSprite2D.play("death")
# 2. 标记史莱姆为死亡状态(停止移动、不再触发玩家碰撞)
is_dead = true
# 3. 销毁击中史莱姆的子弹
area.queue_free()
# 4. 游戏分数+1(访问场景根节点的score变量,需根节点脚本定义该变量)
get_tree().current_scene.score +=1
# 5. 播放死亡音效(绑定的DeathSound音频节点,需确保节点存在)
$DeathSound.play()
# 6. 延迟0.6秒后销毁史莱姆(等待死亡动画播放完成)
# await:暂停函数执行,等待0.6秒定时器超时后再执行queue_free
await get_tree().create_timer(0.6).timeout
queue_free() # 销毁史莱姆节点核心逻辑与细节补充
1. 节点类型选择(Area2D)
史莱姆用Area2D的核心原因:
需同时检测两种碰撞:
✅ 与玩家(CharacterBody2D)的物理体碰撞 → 触发游戏结束(_on_body_entered);
✅ 与子弹(Area2D)的区域碰撞 → 触发死亡逻辑(_on_area_entered);
Area2D轻量且支持多类型碰撞检测,无需物理受力(如被子弹推开),适配史莱姆的核心需求。
2. 关键逻辑拆解
3. 碰撞回调的前提(必配!否则无响应)
代码中的_on_body_entered/_on_area_entered是信号回调函数,需在编辑器绑定信号才会生效:
选中史莱姆
Area2D节点 → 右侧「信号」面板;找到
body_entered信号 → 点击「连接」→ 选择当前脚本的_on_body_entered函数;找到
area_entered信号 → 点击「连接」→ 选择当前脚本的_on_area_entered函数;额外配置:
史莱姆
Area2D需勾选「Monitorable」(可被检测);玩家/子弹节点需设置正确的「Collision Layer/Mask」(确保碰撞层重叠,能检测到彼此)。
4. 代码待优化/注意事项
空值校验缺失:
AnimatedSprite2D/DeathSound 若节点不存在会报错,需补充:
if $AnimatedSprite2D: $AnimatedSprite2D.play("death")
if $DeathSound: $DeathSound.play()分数累加风险:
get_tree().current_scene.score +=1 依赖场景根节点定义score变量,若根节点无该变量会报错,补充:
var main_scene = get_tree().current_scene
if main_scene and main_scene.has("score"):
main_scene.score +=1动画/音效依赖:
需确保AnimatedSprite2D有"death"动画帧、DeathSound是AudioStreamPlayer节点且绑定了音效文件。
5. 整体运行流程
史莱姆被生成 → 存活状态(
is_dead=false)→ 向左移动;若接触玩家 → 调用玩家
game_over()→ 游戏结束;若被子弹击中 → 播放死亡动画/音效 → 分数+1 → 0.6秒后销毁;
若未被击中且移出左边界 → 自动销毁。
这份代码是史莱姆的完整核心逻辑,涵盖了移动、碰撞、死亡、分数累加等所有关键行为,仅需补充信号绑定和空值校验即可稳定运行,是2D休闲游戏中敌人的典型实现方式。
- 感谢你赐予我前进的力量

