看不到代码请重新刷新此页面

源码已上传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()

补充关键细节:

  1. Input.get_vector() 的“归一化”作用:斜向(如同时按上+右)时,向量长度会被修正为1,避免斜移速度比单方向快(比如原本单方向速度50,斜向不归一化会变成≈70.7)。

  2. move_and_slide() 是CharacterBody2D的核心移动方法,相比普通的position修改,它会自动处理碰撞反弹、贴墙移动等物理逻辑,适合玩家角色。

  3. 若想让移动更平滑,也可将_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()
  1. 冗余代码提醒:_physics_process 中出现了两次 move_and_slide(),会导致角色移动逻辑执行两次(速度异常),需删除其中一行;

  2. 动画配置要求:idle/run/game_over 这三个动画名,必须和 AnimatedSprite2D 节点中配置的动画名称完全一致,否则会播放失败;

  3. 节点绑定要求:animator 变量需在 Godot 编辑器的检查器面板中,绑定场景内的 AnimatedSprite2D 节点,否则运行时会报空引用错误;

  4. 输入映射要求: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()
  1. 冗余变量提醒:slime_speed 变量定义后未被使用,代码中硬编码了 Vector2(-100,0),建议优化为 Vector2(slime_speed,0),让速度可通过编辑器调整;

  2. 信号绑定要求:_on_body_entered 函数需要手动绑定 Area2D 的 body_entered 信号才会触发,绑定步骤:

    1. 选中场景中的 Area2D 节点 → 右侧「节点」面板 → 找到 body_entered 信号 → 拖动到脚本节点 → 选择 _on_body_entered 函数;

  3. 碰撞层配置:需确保 Area2D 的「碰撞层 / 掩码」设置正确,能检测到玩家的 CharacterBody2D(比如玩家在 “玩家层”,Area2D 的掩码勾选 “玩家层”);

  4. 方法依赖:body.game_over() 要求玩家的 CharacterBody2D 脚本中必须定义 game_over() 函数,否则运行时会报 “没有该方法” 的错误;

  5. 移动逻辑优化:当前移动是 “绝对速度”,若想让史莱姆速度可配置,可将 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)
  1. 冗余代码_physics_process 中两次调用 move_and_slide(),会导致角色移动速度翻倍、物理逻辑异常,需删除其中一行;

  2. 变量权限is_game_over 无需加 @export(游戏结束状态是内部逻辑,无需在编辑器手动调整),建议移除 @export 避免误改;

  3. 开火逻辑_on_fire() 仅在角色静止且游戏未结束时触发,符合“静止才能发射”的设计,但需给该函数绑定触发方式(如按键):

    # 可在_physics_process函数内添加(放在if not is_game_over判断内)
    if Input.is_action_just_pressed("fire"):  # "fire"需在输入映射中配置(如空格、鼠标左键)
        _on_fire()
  4. 动画依赖idle/run/game_over 动画名需与 AnimatedSprite2D 节点中配置的动画名称完全一致,否则动画播放失败;

  5. 子弹场景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

核心细节与优化提醒:

  1. 缩进规范:若之前报「Mixed use of tabs and spaces」错误,需确保所有缩进统一为4个空格(GDScript官方推荐),可通过Godot编辑器「编辑→转换缩进→转换为空格」批量修正;

  2. 定时器优化_ready()await get_tree().create_timer(3) 可补充 timeout 信号(写法更规范),效果一致:

    await get_tree().create_timer(3).timeout
  3. 碰撞逻辑补充:子弹是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()       # 销毁子弹
  4. 方向控制:若需要子弹向左移动,只需将 bullet_speed 设为负数(如-100),无需修改移动逻辑;

  5. 内存管理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  # 必须加.timeout

create_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

快速验证方法

  1. 全选你的代码,删除后粘贴上面的修正版;

  2. 在Godot编辑器中点击「编辑→转换缩进→转换为空格」,确保缩进统一;

  3. 运行游戏,按开火键,子弹会正常发射并向右移动,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)

核心逻辑解释

  1. _ready() 中绑定信号 + 启动定时器:是定时器生效的前提;

  2. _process() 中动态修改等待时间:实现 “生成速度越来越快(直到 1 秒 / 个)” 的效果;

  3. _on_spawn_timer_timeout() 回调:定时器到点后生成史莱姆并重启定时器,实现循环生成;

  4. _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.ZEROvelocityCharacterBody2D 的核心属性(速度向量),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 信号绑定到该函数(游戏结束后,定时器倒计时结束自动重启)。

七、代码整体逻辑梳理

  1. 正常游戏状态

    1. 玩家通过方向键/WSAD移动,_physics_process 处理移动和动画;

    2. 移动时播放跑步音效,静止时停止;

    3. 调用 _on_fire() 时生成子弹(需绑定输入/信号触发,比如J键)。

  2. 游戏结束状态

    1. 调用 game_over() 后,is_game_over 设为true,停止移动逻辑;

    2. 播放结束动画、音效,显示结束UI,启动重启定时器;

    3. 定时器超时后调用 _reload_scene() 重启场景。

八、代码待完善/注意事项

  1. _on_fire() 未绑定触发逻辑:当前代码中 _on_fire() 只是定义了,但没有绑定到J键输入/按钮点击,需补充输入监听(比如之前提到的J键按下触发);

  2. 缺少空值检查:比如 bullet_scene 未绑定预制体时,instantiate() 会报错,需加 if bullet_scene: 校验;

  3. 子弹生成位置固定:Vector2(6,6) 是硬编码,可改为导出变量方便调整;

  4. 输入映射依赖: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()  # 重新加载当前场景,实现游戏重启

核心逻辑总结(补充说明)

  1. 射击机制核心:用 Timer 控制发射间隔(避免按住 J 键帧级发射),Input.is_action_just_pressed/released 监听 J 键的「按下 / 松开瞬间」,分别启动 / 停止定时器,实现「按住 J 连续发射、松开停止」的效果;

  2. 状态校验_on_fire 中多重校验(射击状态 / 定时器状态 / 游戏状态),避免无效发射(比如游戏结束后仍发射、定时器意外启动等);

  3. 兼容性优化:移除了 Timer 的stopped信号绑定(避免版本兼容问题),改用is_stopped()方法判断定时器状态,手动同步is_firing标记;

  4. 调试友好:关键节点添加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. 关键方法解析

代码片段

作用说明

await get_tree().create_timer(3).timeout

临时创建3秒定时器,await 会阻塞_ready函数,直到3秒后才执行queue_free

queue_free()

安全销毁节点:等待当前帧结束后移除节点,避免直接销毁导致的内存错误

position += Vector2(...) * delta

手动修改坐标实现移动:delta 是物理帧间隔(约1/60秒),保证不同帧率下移动速度一致

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. 关键代码解析

代码片段

核心作用

注意事项

spawn_timer.wait_time -= 0.2 * delta

每帧减少史莱姆生成间隔,实现“越往后生成越快”的难度递增

需确保spawn_timer已在编辑器绑定有效Timer节点,否则会报“Nil”错误

clamp(spawn_timer.wait_time,1,3)

限制生成间隔在1~3秒之间:<br>✅ 初始间隔3秒(慢)→ 逐渐减少 → 最终稳定在1秒(快)<br>✅ 防止间隔为负数(定时器立即触发)或超过3秒(难度过低)

clamp(值, 最小值, 最大值) 是GDScript内置函数,强制值在指定范围

score_label.text = "Score:"+str(score)

实时更新分数UI:<br>- str(score):将整数转为字符串(Label的text仅接受字符串)<br>- 拼接成“Score:0”“Score:5”等格式显示

需确保score_label绑定了场景中的Label节点,且节点启用(visible=true)

_spawn_slime()

生成史莱姆:<br>① 实例化预制体 → ② 随机Y轴位置 → ③ 添加到场景

需配合spawn_timertimeout信号使用(定时器超时后调用该函数),否则不会自动生成史莱姆

show_game_over()

游戏结束时显示提示标签

需确保game_over_label初始状态visible=false(隐藏),调用后才显示

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. 整体运行流程(补充后)

  1. 游戏启动 → _ready() 启动定时器,绑定生成函数;

  2. 每帧执行_process():<br>→ 减少定时器间隔(难度递增)→ 实时更新分数显示;

  3. 定时器超时 → 调用_spawn_slime()生成史莱姆;

  4. 玩家击中史莱姆 → 场景根节点的score+1 → 分数标签实时更新;

  5. 玩家碰撞史莱姆 → 调用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. 关键逻辑拆解

模块

核心行为

细节说明

移动逻辑

存活时向左移动,超出左边界销毁

slime_speed=-100 → X轴负方向(左);position.x < -267 是屏幕左边界阈值(可根据分辨率调整)

玩家碰撞逻辑

接触玩家且存活时,触发游戏结束

body is CharacterBody2D 精准判断碰撞对象是玩家;not is_dead 避免死亡后重复触发

子弹碰撞逻辑

被子弹击中后:<br>① 播放死亡动画/音效 → ② 标记死亡 → ③ 销毁子弹 → ④ 加分 → ⑤ 延迟销毁自身

area.is_in_group("bullet") 需给子弹节点添加"bullet"分组(编辑器中设置);await 等待动画播放完成再销毁

状态控制

is_dead 作为核心开关,控制所有行为

死亡后停止移动、不再触发玩家碰撞,避免逻辑混乱(比如“死了还能撞玩家”)

3. 碰撞回调的前提(必配!否则无响应)

代码中的_on_body_entered/_on_area_entered信号回调函数,需在编辑器绑定信号才会生效:

  1. 选中史莱姆Area2D节点 → 右侧「信号」面板;

  2. 找到body_entered信号 → 点击「连接」→ 选择当前脚本的_on_body_entered函数;

  3. 找到area_entered信号 → 点击「连接」→ 选择当前脚本的_on_area_entered函数;

  4. 额外配置:

    1. 史莱姆Area2D需勾选「Monitorable」(可被检测);

    2. 玩家/子弹节点需设置正确的「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"动画帧、DeathSoundAudioStreamPlayer节点且绑定了音效文件。

5. 整体运行流程

  1. 史莱姆被生成 → 存活状态(is_dead=false)→ 向左移动;

  2. 若接触玩家 → 调用玩家game_over() → 游戏结束;

  3. 若被子弹击中 → 播放死亡动画/音效 → 分数+1 → 0.6秒后销毁;

  4. 若未被击中且移出左边界 → 自动销毁。

这份代码是史莱姆的完整核心逻辑,涵盖了移动、碰撞、死亡、分数累加等所有关键行为,仅需补充信号绑定和空值校验即可稳定运行,是2D休闲游戏中敌人的典型实现方式。