)
以下是人物的脚本实现了死亡动画播放完毕后显示“菜”字且“菜”字自适应屏幕居中无论窗口大小如何变化都会居中显示。# 1. 继承 CharacterBody2D 节点 # 作用让当前脚本挂载的节点拥有2D角色移动、碰撞、物理处理的基础能力 extends CharacterBody2D # 可编辑导出变量编辑器可见 # 2. 导出移动速度变量默认300可在编辑器直接修改 export var speed: float 300.0 # 3. 导出子弹场景变量用于在编辑器拖拽绑定子弹预制体 export var bullet_scene: PackedScene # 4. 导出射击冷却时间控制射速默认0.15秒/发 export var shoot_cooldown: float 0.15 # 节点引用自动获取子节点 # 5. 获取名为 AnimatedSprite2D 的子节点用于播放动画 onready var animated_sprite: AnimatedSprite2D $AnimatedSprite2D # 6. 获取名为 Muzzle 的枪口标记点子弹从这里生成 onready var muzzle: Marker2D $Muzzle # 7. 获取名为 Hurtbox 的受伤区域用于检测敌人碰撞 onready var hurtbox: Area2D $Hurtbox # 自定义状态变量 # 8. 声明射击计时器控制射速 var shoot_timer: Timer # 9. 射击状态是否正在按住射击键 var is_shooting: bool false # 10. 死亡状态玩家是否死亡 var is_dead: bool false # 11. 防开局自动开枪游戏启动后延迟允许射击 var can_shoot_after_ready: bool false # 初始化函数节点加载完成执行 func _ready(): # 12. 将玩家加入 player 组方便全局管理比如敌人检测玩家 add_to_group(player) # 13. 新建一个计时器节点 shoot_timer Timer.new() # 14. 设置计时器等待时间 射击冷却时间 shoot_timer.wait_time shoot_cooldown # 15. 非单次计时循环触发按住射击键持续开枪 shoot_timer.one_shot false # 16. 绑定计时器超时信号时间到执行 _on_shoot_timer_timeout 函数 shoot_timer.timeout.connect(_on_shoot_timer_timeout) # 17. 将计时器添加为当前节点的子节点 add_child(shoot_timer) # 18. 如果受伤区域存在 if hurtbox: # 19. 绑定受伤区域的碰撞信号碰到物体执行 _on_hurtbox_body_entered hurtbox.body_entered.connect(_on_hurtbox_body_entered) # 20. 等待0.1秒再开启射击权限 await get_tree().create_timer(0.1).timeout # 21. 允许射击 can_shoot_after_ready true # 物理帧更新固定频率处理移动/输入 func _physics_process(delta: float) - void: # 22. 如果玩家死亡直接退出函数不执行后续逻辑 if is_dead: return # 23. 获取鼠标在世界中的全局坐标 var mouse_pos get_global_mouse_position() # 24. 获取水平输入左(-1)/右(1)/无(0)对应项目设置的输入映射 var horizontal : Input.get_axis(move_left, move_right) # 25. 获取垂直输入上(-1)/下(1)/无(0) var vertical : Input.get_axis(move_up, move_down) # 26. 组合成方向向量并标准化防止斜向移动速度过快 var direction : Vector2(horizontal, vertical).normalized() # 27. 计算速度 方向 * 速度 velocity direction * speed # 28. 执行移动CharacterBody2D 自带函数处理碰撞 move_and_slide() # 29. 如果动画节点存在 if animated_sprite: # 30. 如果玩家在移动 if velocity.length() 0: # 31. 播放走路动画 animated_sprite.play(walk) # 32. 如果水平方向有输入翻转精灵向左走就向左看 if horizontal ! 0: animated_sprite.flip_h horizontal 0 else: # 33. 没移动就播放 idle 待机动画 animated_sprite.play(idle) # 34. 强制角色朝向鼠标鼠标在左边精灵向左翻转 if animated_sprite and mouse_pos.x global_position.x: animated_sprite.flip_h true # 35. 鼠标在右边精灵向右 elif animated_sprite: animated_sprite.flip_h false # 36. 如果按住射击键 if Input.is_action_pressed(shoot): # 37. 如果没在射击状态 if not is_shooting: # 38. 开始射击 start_shooting() else: # 39. 松开射击键且正在射击 if is_shooting: # 40. 停止射击 stop_shooting() # 开始射击函数 func start_shooting(): # 41. 死亡 或 未到允许射击时间 → 直接退出 if is_dead or not can_shoot_after_ready: return # 42. 标记为正在射击 is_shooting true # 43. 立即向鼠标位置发射一颗子弹 shoot_at(get_global_mouse_position()) # 44. 启动射速计时器 shoot_timer.start() # 停止射击函数 func stop_shooting(): # 45. 取消射击状态 is_shooting false # 46. 停止计时器 shoot_timer.stop() # 计时器超时回调自动连射 func _on_shoot_timer_timeout(): # 47. 按住射击键 玩家存活 → 继续射击 if is_shooting and not is_dead: shoot_at(get_global_mouse_position()) # 发射子弹核心函数 func shoot_at(target_pos: Vector2): # 48. 死亡 或 没绑定子弹场景 → 退出 if is_dead or not bullet_scene: return # 49. 计算子弹飞行方向目标点 - 玩家位置 → 标准化 var direction (target_pos - global_position).normalized() # 50. 实例化子弹场景 var bullet bullet_scene.instantiate() # 51. 设置子弹位置优先用枪口没有就用玩家自身位置 bullet.global_position muzzle.global_position if muzzle else global_position # 52. 给子弹赋值飞行方向 bullet.direction direction # 53. 给子弹赋值飞行速度 bullet.speed 400 # 54. 将子弹添加到当前场景 get_tree().current_scene.add_child(bullet) # 受伤区域碰撞回调 func _on_hurtbox_body_entered(body: Node2D): # 55. 已死亡 → 退出 if is_dead: return # 56. 碰到的物体是敌人 → 执行死亡 if body.is_in_group(enemy): die() # 死亡逻辑 func die(): # 57. 防止重复死亡 if is_dead: return # 58. 标记为死亡 is_dead true # 59. 关闭物理更新停止移动 set_physics_process(false) # 60. 如果正在射击停止射击 if is_shooting: stop_shooting() # 61. 获取玩家碰撞体 var collision_shape $CollisionShape2D if collision_shape: # 62. 禁用碰撞死亡后无法被碰撞 collision_shape.disabled true if hurtbox: # 63. 关闭受伤区域检测 hurtbox.monitoring false # 64. 如果有死亡动画播放动画 if animated_sprite and animated_sprite.sprite_frames.has_animation(death): animated_sprite.play(death) # 65. 等待动画播放完成 await animated_sprite.animation_finished # 66. 隐藏玩家 visible false # 67. 显示“菜”字提示 show_cai_word() # 68. 暂停游戏 get_tree().paused true # 显示死亡提示文字 func show_cai_word(): # 69. 创建画布层确保文字显示在最上层 var canvas CanvasLayer.new() canvas.layer 10 # 70. 游戏暂停时依然正常运行 canvas.process_mode Node.PROCESS_MODE_ALWAYS get_tree().current_scene.add_child(canvas) # 71. 创建文字标签 var label Label.new() label.text 菜 # 提示文字 label.horizontal_alignment HORIZONTAL_ALIGNMENT_CENTER # 水平居中 label.vertical_alignment VERTICAL_ALIGNMENT_CENTER # 垂直居中 label.add_theme_font_size_override(font_size, 120) # 字体大小120 label.add_theme_color_override(font_color, Color.RED) # 红色字体 label.anchor_right 1.0 # 锚点铺满屏幕 label.anchor_bottom 1.0 label.offset_left 0 label.offset_top 0 label.size Vector2.ZERO label.process_mode Node.PROCESS_MODE_ALWAYS # 暂停时显示 canvas.add_child(label)接下来开始拆解一、onreadyonready是Godot 4 专门用来获取子节点的语法糖作用只有一个等节点准备好之后再自动获取子节点不用自己写在 _ready () 里。场景节点只有进入场景树、触发_ready()生命周期后才能用get_node()找到子节点 普通变量在脚本最顶部定义时执行时机早于节点加载直接赋值找不到节点所以要写在_ready()里onready修饰的变量会自动把赋值语句推迟到_ready()执行阶段运行简化代码。另外说一句在GDscript中$ get_node()而$节点名就是get_node(节点名)的简写# 两种写法完全等同 onready var my_label get_node(MyLabel) onready var my_label $MyLabel我们可以用onready来对全局变量声明_ready()函数会在所有onready 变量赋值全部完成后才运行函数里的自定义代码这样就可以解决下述问题全局变量先创建此时节点还没加载进场景不能直接get_node()必须等到_ready()函数运行节点全部就绪再在函数内部获取节点存入变量节点多了_ready里会堆满一堆xxxget_node()代码臃肿。二、拆解export var bullet_scene: PackedScenePackedScene保存成资源的场景模板对标 Unity 预制体 Prefab属于Resource资源类型存着一整棵节点树的配置数据不能直接显示必须调用.instantiate()生成真实节点实体核心它是 Resource资源不是 Node节点Node能放进场景树、能渲染、有坐标、能运行_process玩家、子弹、Sprite 都属于 NodePackedScene只是硬盘上.tscn文件加载到内存的数据包没有坐标、不在场景树、画面看不见只负责「复制生成整套节点」1.三种在代码获取 PackedScene 的写法①. export 拖拽赋值export var bullet_scene: PackedScene选中玩家节点右侧检查器面板直接把Bullet.tscn拖入变量框编辑器自动加载资源运行前就绑定好新手最常用。②. preload编译时预加载推荐固定资源const BULLET_RES: PackedScene preload(res://scenes/Bullet.tscn)引擎编译脚本时直接读取 tscn运行零加载卡顿写死路径的子弹 / 敌人首选。③. load运行时动态加载路径可变var res_path res://scenes/Bullet.tscn var bullet_res: PackedScene load(res_path)游戏运行中按需读取适合动态换皮肤、动态加载关卡资源。2、核心方法.instantiate()生成子弹的关键#1. 数据包PackedScene看不见 var bullet_res: PackedScene bullet_scene #2. instantiate根据数据包在内存生成完整节点树子弹实体不在场景 var bullet_node bullet_res.instantiate() #3. add_child加入场景树引擎渲染、物理生效、屏幕出现子弹 get_tree().current_scene.add_child(bullet_node).instantiate()关键特性①.每次 instantiate 都是全新独立个体改 A 子弹的速度不会改 B 子弹、不会改原 tscn 模板②.生成瞬间只执行子弹脚本_init()add_child 进场景后才触发 onready、_ready ()3.PackedScene VS 普通节点 new () 巨大区别#方式1PackedScene实例化推荐子弹/怪物 var bullet bullet_scene.instantiate() #自动生成根节点Sprite2D碰撞形状绑定好的脚本编辑器调好的参数 #方式2手动new节点极麻烦不适合大量生成 var bullet CharacterBody2D.new() var spr Sprite2D.new() bullet.add_child(spr) #所有贴图、碰撞、参数全部代码手动写效率极低PackedScene 一次性还原整套节点层级 所有编辑器配置这是它最大价值。4.配套常用内置 API①.can_instantiate()判断 PackedScene 资源完好、可以实例化防止资源丢失报错if bullet_scene.can_instantiate(): bullet_scene.instantiate()②.pack(node:Node)运行时把场景树上现成节点打包成新 PackedScene动态生成预制var new_pack PackedScene.new() new_pack.pack($Enemy) ResourceSaver.save(new_pack,res://new_enemy.tscn)③.总结PackedScene数据类型PackedScene 是 Godot 中代表场景模板.tscn的数据类型它本身只是一份资源数据不显示在场景中。通过调用它的内置方法.instantiate()可以将编辑器里制作好的完整场景一次性实例化为可运行的节点对象并自动带入该场景的节点结构、精灵、碰撞体、脚本、变量、信号、动画等所有配置无需在代码里重复创建节点、绑定功能极大简化动态生成对象的逻辑。三、Marker2D节点讲实话我也根本不大明白这到底是什么意思从定义出发1.根据定义来说Marker2D 是一个通用的 2D 位置标记节点用于编辑时可视化标记一个点。它继承自Node2D和普通 Node2D 功能几乎一样只有一个区别在编辑器里永远显示一个绿色十字方便你看见它在哪运行游戏时完全隐形、不渲染、不画任何东西。Marker2D 一个 “看得见的空点”。它没有图片、没有碰撞、不显示在游戏画面里。它只是一个坐标点位置 旋转。编辑器里绿色十字一眼能找到。运行时完全消失不影响画面和性能。而Marker2D的唯一属性gizmo_extents默认 10.0此属性其实就是调整编辑器中绿色十字的大小而已只影响编辑器的显示不影响游戏Marker2D 就是个轻量 Node2D成百上千个也不卡。这就是为什么这里的子弹选择使用Marker2D节点2.总结将Marker2D这个点放在场景的任意一个点 Marker2D这个点绑定上含有它的脚本的场景比如人脚本里面含有onready var muzzle: Marker2D $Muzzle 这样Marker2D就会和人物场景绑定然后我在编辑器里面放置Marker2D的位置在游戏运行中 Marker2D节点都只是会进行相对于人物的运动四、为什么使用Area2D来发挥Hurtbox作用依旧从定义入手来看看官方文档可以看见Area2D作用主要是检测其他CollisionObject2D的进入或退出也就是检测碰撞体那么对于我们用来作为Hurtbox来说是一个绝佳的节点Hurtbox只需要检测它的核心需求就一个检测重叠不影响物理移动不产生阻挡 / 反弹1.那么我们为什么不适用碰撞体呢所有碰撞体都会受力也有的会被阻挡我们的目的是在特定的区域内检测受伤只有 Area2D 满足所有条件✅只检测重叠不产生物理阻挡子弹穿过你你只掉血不会被推✅不影响角色移动、跳跃、碰撞✅可以单独画形状头、身体、四肢分开做暴击判定✅有信号area_entered /body_entered代码好写✅性能轻量无数个也不卡层与掩码精准过滤Hurtbox 层设为「受伤层」Hitbox子弹 / 武器掩码只扫「受伤层」不会误触发墙、地面、队友信号驱动代码干净# Hurtbox 脚本 func _ready(): area_entered.connect(_on_hit) func _on_hit(area): if area.is_in_group(bullet): take_damage(area.damage)且主要原因是Area2D有着只“感知”不干涉的优势也是其可以作为Hurtbox最重要的一点五、层与掩码层Layer 我是谁掩码Mask 我能看到谁。适用于所有 2D 物理节点Area2D、CharacterBody2D、RigidBody2D…。1.HurtboxArea2D举最实用场景结构PlayerCharacterBody2DHurtboxArea2D← 受伤判定区1Player 设置Layer1PlayerMask2,5只和敌人、墙壁碰撞不检测子弹2HurtboxArea2D设置Layer4Hurtbox← 我是受伤区Mask3Bullet← 我只检测子弹3BulletArea2D设置Layer3Bullet← 我是子弹Mask4Hurtbox← 我只检测受伤区效果子弹碰到玩家身体Layer1→忽略Mask 不包含 1子弹碰到 HurtboxLayer4→触发受伤信号Mask 包含 4玩家不会被子弹挡住因为 Player 的 Mask 不含 Bullet总结一下其实就是当layer mask 即可触发检测 / 碰撞六、Timer节点与其他语言不同的是 GDscript的Timer是倒计时器而非计时器1.定义 Timer 倒计时器从设定时间开始倒数到 0 时发出timeout 信号可以只跑一次或循环重复不依赖帧率、不卡主线程最适合做技能 CD、攻击间隔、生成怪物、无敌时间2.核心属性Inspector 面板①. Wait Time最常用类型float秒含义每次倒计时的总时间例子1.0 1 秒后触发 timeout②.One Shot一次 / 循环true只执行一次到 0 就停适合 “3 秒后开门”false循环执行到 0 自动重置再倒数适合 “每 2 秒发一颗子弹”③. Autostart自动启动true进入场景就自动开始倒计时false必须手动start()才开始常用④. Ignore Time Scale忽略慢动作false受Engine.time_scale影响慢动作时倒计时变慢true真实时间不受慢动作影响适合 UI、剧情倒计时⑤. Process Mode更新时机Idle默认在_process更新和帧率同步适合 UI、动画Physics在_physics_process更新固定 60 次 / 秒适合攻击、移动、物理相关冷却3.唯一信号timeoutTimer 到 0 时自动发出你要做的把这个信号连接到你的函数编辑器连接推荐选中 Timer → 右侧「节点」→ 信号 → 双击timeout选目标节点如玩家→ 选脚本函数如_on_attack_cd自动生成代码func _on_attack_cd(): print(技能冷却结束)代码连接timer.timeout.connect(_on_attack_cd)④、常用方法代码控制timer.start() # 开始倒计时重置并启动 timer.stop() # 停止并归零 timer.paused true # 暂停保留剩余时间 timer.paused false # 继续 timer.time_left # 剩余时间只读 timer.is_active() # 是否正在运行4.实战场景直接抄①. 玩家攻击冷却核心需求每 0.5 秒才能攻击一次Timer 设置Wait Time 0.5One Shot false循环Autostart trueProcess Mode Physicsfunc _input(event): if event is InputEventMouseButton and event.pressed: if timer.is_active() false: # 冷却结束 attack() timer.start() # 重新开始冷却②. 3 秒后生成敌人一次性TimerWait Time3One ShottrueAutostarttruefunc _on_timer_timeout(): spawn_enemy()③. 无敌时间受伤后 1 秒无敌func take_damage(): if not invincible: health - 1 invincible true invincible_timer.start() # 1秒后解除无敌 func _on_invincible_timer_timeout(): invincible false5.避坑要点不要用 _process 做倒计时帧率不稳、代码乱、容易累积误差。技能冷却一定要用 Physics 模式和物理步同步不会因为掉帧导致攻击变快。One Shot true 时timeout 后不会自动重启必须手动start()。不要重复连接信号否则一次 timeout 触发多次函数。七、组group1.定义Godot 的Group 全局标签 / 分类一个节点可以加多个组不依赖父子层级、不依赖类型全场景、跨场景都能查到玩家加入player组 告诉引擎这个节点是玩家全世界都能快速找到我。2.为什么一定要把玩家加入组①. 快速 “找到玩家”任何敌人、陷阱、系统都需要知道玩家在哪、玩家是谁。不用组# 硬找、容易错、换场景就崩 var player get_node(/root/Player)用组# 安全、简单、跨场景也能用 var players get_tree().get_nodes_in_group(player) if players.size() 0: var player players[0]一句话组 全局快速索引②. 批量发消息全局事件比如全屏震动、所有敌人发现玩家、游戏暂停。# 给所有玩家发“受伤” get_tree().call_group(player, take_damage, 1) # 给所有玩家发“暂停” get_tree().call_group(player, set_paused, true)一句话组 广播器不用一个个连信号③. 解耦合代码不写死、好维护代码敌人脚本不需要硬引用玩家节点只认组# 敌人AI找玩家 func _find_player(): var players get_tree().get_nodes_in_group(player) if players: target players[0]玩家删了、换了、改名了敌人代码完全不用改。④. 多玩家 / 多角色兼容扩展性强以后做多人游戏、分身、队友都加入player组现有逻辑全部自动兼容不用大改代码。⑤.身份判断Area2D、碰撞、触发器必用比如你有一个伤害区 / 触发区Area2D不用组# 写死节点名改名字就崩 if body.name Player: take_damage()用组if body.is_in_group(player): take_damage()一句话组 安全的身份识别标签八、解析初始化函数# 初始化函数节点加载完成执行 func _ready(): # 12. 将玩家加入 player 组方便全局管理比如敌人检测玩家 add_to_group(player) # 13. 新建一个计时器节点 shoot_timer Timer.new() # 14. 设置计时器等待时间 射击冷却时间 shoot_timer.wait_time shoot_cooldown # 15. 非单次计时循环触发按住射击键持续开枪 shoot_timer.one_shot false # 16. 绑定计时器超时信号时间到执行 _on_shoot_timer_timeout 函数 shoot_timer.timeout.connect(_on_shoot_timer_timeout) # 17. 将计时器添加为当前节点的子节点 add_child(shoot_timer) # 18. 如果受伤区域存在 if hurtbox: # 19. 绑定受伤区域的碰撞信号碰到物体执行 _on_hurtbox_body_entered hurtbox.body_entered.connect(_on_hurtbox_body_entered) # 20. 等待0.1秒再开启射击权限 await get_tree().create_timer(0.1).timeout # 21. 允许射击 can_shoot_after_ready true1. .new( )方法Timer.new () 创建一个全新的计时器对象Label.new()→ 创建新文字Sprite2D.new()→ 创建新图片Marker2D.new()→ 创建新标记点也就是创建一个全新的节点对象下面的add_child(shoot_timer)是必须加的此行代码表示把shoot_timer这个计时器添加卫当前节点的子节点如果不添加Timer就没有被添加就不会起作用2.为什么玩家开枪非要新建一个 Timer 节点1.因为需要【按住鼠标 → 自动连续开枪】必须用计时器来控制【每多少秒打一枪】 不用计时器你根本做不出稳定、不卡、不依赖帧率的连射如果不用计时器只能这样写func _process(delta): if Input.get_mouse_button_mask() MOUSE_BUTTON_LEFT: shoot() # 一直开枪这样就会导致电脑性能好 →开枪快到飞起电脑卡顿 →开枪变得巨慢一秒钟能射 100 发完全失控2.每个独立功能都需要自己独立的计时器开枪冷却 → 1 个 Timer技能冷却 → 1 个 Timer无敌时间 → 1 个 Timer换弹时间 → 1 个 Timer它们时间不一样、逻辑不一样必须分开不能共用3.计时器是 “工具节点”专门管时间Godot 里想计时 →必须用 Timer想稳定间隔 →必须用 Timer想做 CD →必须用 TimerTimer 就是专门用来干这个的标准工具。4.代码创建 Timer 更干净、更自动不在编辑器里占位置脚本复制到任何角色都能直接用不需要手动拖节点、设置参数完全自动化不易出错