4. 敌人交互
2026/4/14大约 6 分钟
4. 超级玛丽奥——敌人交互
简介
敌人交互是超级玛丽奥的战斗核心——玛丽奥如何与敌人"战斗"。和其他游戏不同,玛丽奥的主要攻击方式不是射击或挥剑,而是踩踏。从敌人头上跳过去踩扁它,这就是打败敌人的方法。
但如果你从侧面碰到敌人,受伤的就不是敌人,而是玛丽奥自己。所以判断"从上面踩"还是"从侧面碰"是关键。
敌人行为概述
| 敌人 | 移动方式 | 被踩后的反应 | 特殊行为 |
|---|---|---|---|
| 蘑菇怪 | 一直往前走 | 被踩扁,消失 | 碰到障碍物转向 |
| 乌龟 | 来回走动 | 缩进龟壳 | 龟壳可以被踢飞 |
| 飞乌龟 | 飞行 | 掉翅膀变普通乌龟 | 上下或左右飞 |
踩踏检测原理
踩踏检测的核心原理很简单:比较玛丽奥和敌人的Y坐标。如果玛丽奥的底部在敌人头部的上方,就算踩踏。
情况1:踩踏(玛丽奥在上方)
玛丽奥
┌──┐
│ │ ← 玛丽奥底部 Y = 100
└──┘
┌──┐
│蘑│ ← 敌人顶部 Y = 110
│菇│
└──┘
玛丽奥底部 (100) < 敌人顶部 (110) → 踩踏!
情况2:被碰(玛丽奥在侧面)
┌──┐ ┌──┐
│蘑│ │玛│ ← 两者的Y坐标差不多
│菇│ │丽│
└──┘ └──┘
玛丽奥底部 > 敌人顶部 → 被碰!敌人基类
// Enemy.cs - 敌人基类
using Godot;
public partial class Enemy : CharacterBody2D
{
// ========== 属性 ==========
[Export] public float MoveSpeed { get; set; } = 60.0f;
[Export] public float Gravity { get; set; } = 980.0f;
// 移动方向
protected int _direction = -1; // -1=左, 1=右
// 是否存活
protected bool _isAlive = true;
// 节点引用
protected AnimatedSprite2D _sprite;
protected CollisionShape2D _collisionShape;
// 信号
[Signal] public delegate void EnemyDiedEventHandler(int score);
public override void _Ready()
{
_sprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
_collisionShape = GetNode<CollisionShape2D>("CollisionShape2D");
}
public override void _PhysicsProcess(double delta)
{
if (!_isAlive) return;
float dt = (float)delta;
// 应用重力
if (!IsOnFloor())
{
Velocity += new Vector2(0, Gravity * dt);
}
// 水平移动
Velocity = new Vector2(_direction * MoveSpeed, Velocity.Y);
// 移动并检测碰撞
MoveAndSlide();
// 碰到墙壁就转向
if (IsOnWall())
{
_direction *= -1;
_sprite.FlipH = (_direction < 0);
}
}
// 被踩踏(子类重写具体行为)
public virtual void Stomped()
{
Die();
GameManager.Instance.AddScore(100);
}
// 死亡
public virtual void Die()
{
if (!_isAlive) return;
_isAlive = false;
// 播放死亡动画
_sprite.Play("death");
// 禁用碰撞
SetCollisionLayerValue(2, false);
// 延迟后销毁
var timer = GetTree().CreateTimer(0.5);
timer.Timeout += QueueFree;
}
// 掉出屏幕(掉坑)
private void OnVisibilityNotifier2DScreenExited()
{
QueueFree();
}
}# enemy.gd - 敌人基类
extends CharacterBody2D
# ========== 属性 ==========
@export var move_speed: float = 60.0
@export var gravity: float = 980.0
# 移动方向
var direction: int = -1 # -1=左, 1=右
# 是否存活
var is_alive: bool = true
# 节点引用
@onready var sprite: AnimatedSprite2D = $AnimatedSprite2D
@onready var collision_shape: CollisionShape2D = $CollisionShape2D
# 信号
signal enemy_died(score: int)
func _ready() -> void:
pass
func _physics_process(delta: float) -> void:
if not is_alive:
return
# 应用重力
if not is_on_floor():
velocity += Vector2(0, gravity * delta)
# 水平移动
velocity = Vector2(direction * move_speed, velocity.y)
# 移动并检测碰撞
move_and_slide()
# 碰到墙壁就转向
if is_on_wall():
direction *= -1
sprite.flip_h = (direction < 0)
## 被踩踏(子类重写具体行为)
func stomped() -> void:
die()
GameManager.add_score(100)
## 死亡
func die() -> void:
if not is_alive:
return
is_alive = false
# 播放死亡动画
sprite.play("death")
# 禁用碰撞
set_collision_layer_value(2, false)
# 延迟后销毁
var timer = get_tree().create_timer(0.5)
timer.timeout.connect(queue_free)
## 掉出屏幕
func _on_visibility_notifier_2d_screen_exited() -> void:
queue_free()蘑菇怪(Goomba)
蘑菇怪是最简单的敌人——一直往前走,碰到墙就转向,被踩就死。
// Goomba.cs - 蘑菇怪
public partial class Goomba : Enemy
{
public override void _Ready()
{
base._Ready();
_sprite.Play("walk");
}
public override void Stomped()
{
if (!_isAlive) return;
// 蘑菇怪被踩扁:播放扁平动画然后消失
_isAlive = false;
_sprite.Play("squished");
// 停止移动
Velocity = Vector2.Zero;
// 禁用碰撞
SetCollisionLayerValue(2, false);
// 加分
GameManager.Instance.AddScore(100);
// 延迟后销毁
var timer = GetTree().CreateTimer(0.3);
timer.Timeout += QueueFree;
}
}# goomba.gd - 蘑菇怪
extends "res://scripts/enemies/enemy.gd"
func _ready() -> void:
super._ready()
sprite.play("walk")
func stomped() -> void:
if not is_alive:
return
# 蘑菇怪被踩扁:播放扁平动画然后消失
is_alive = false
sprite.play("squished")
# 停止移动
velocity = Vector2.ZERO
# 禁用碰撞
set_collision_layer_value(2, false)
# 加分
GameManager.add_score(100)
# 延迟后销毁
var timer = get_tree().create_timer(0.3)
timer.timeout.connect(queue_free)乌龟(Koopa)
乌龟比蘑菇怪复杂——踩一下会缩进龟壳,龟壳可以被踢飞。
// Koopa.cs - 乌龟
public partial class Koopa : Enemy
{
// 乌龟状态
public enum KoopaState
{
Walking, // 正常行走
Shell, // 缩在壳里(静止)
ShellMoving // 龟壳在滑动
}
private KoopaState _state = KoopaState.Walking;
private float _shellSpeed = 400.0f; // 龟壳滑动速度
public override void _Ready()
{
base._Ready();
_sprite.Play("walk");
}
public override void _PhysicsProcess(double delta)
{
if (_state == KoopaState.Walking)
{
base._PhysicsProcess(delta);
}
else if (_state == KoopaState.ShellMoving)
{
// 龟壳快速滑动
float dt = (float)delta;
if (!IsOnFloor())
{
Velocity += new Vector2(0, Gravity * dt);
}
Velocity = new Vector2(_direction * _shellSpeed, Velocity.Y);
MoveAndSlide();
}
// Shell 状态静止不动
}
public override void Stomped()
{
if (!_isAlive) return;
switch (_state)
{
case KoopaState.Walking:
// 第一次踩:缩进壳里
EnterShell();
GameManager.Instance.AddScore(100);
break;
case KoopaState.Shell:
// 踩静止的龟壳:开始滑动
StartShellMoving();
GameManager.Instance.AddScore(200);
break;
case KoopaState.ShellMoving:
// 踩滑动的龟壳:停下来
StopShell();
GameManager.Instance.AddScore(100);
break;
}
}
// 缩进壳里
private void EnterShell()
{
_state = KoopaState.Shell;
Velocity = Vector2.Zero;
_sprite.Play("shell");
}
// 龟壳开始滑动
private void StartShellMoving()
{
_state = KoopaState.ShellMoving;
_sprite.Play("shell_moving");
// 朝向踩它的人的方向滑
var player = GetTree().GetFirstNodeInGroup("player");
if (player != null)
{
_direction = (player.Position.X > Position.X) ? 1 : -1;
}
}
// 龟壳停下来
private void StopShell()
{
_state = KoopaState.Shell;
Velocity = Vector2.Zero;
_sprite.Play("shell");
}
// 被侧面碰到
public void SideHit()
{
if (_state == KoopaState.Shell)
{
// 踢静止的龟壳
StartShellMoving();
}
else if (_state == KoopaState.ShellMoving)
{
// 滑动的龟壳碰到东西停下
StopShell();
}
}
// 龟壳撞到其他敌人
private void OnBodyEntered(Node2D body)
{
if (_state == KoopaState.ShellMoving && body is Enemy otherEnemy && otherEnemy != this)
{
// 滑动的龟壳撞死其他敌人
otherEnemy.Die();
GameManager.Instance.AddScore(200);
}
}
}# koopa.gd - 乌龟
extends "res://scripts/enemies/enemy.gd"
# 乌龟状态
enum KoopaState {
WALKING, # 正常行走
SHELL, # 缩在壳里(静止)
SHELL_MOVING # 龟壳在滑动
}
var state: int = KoopaState.WALKING
var shell_speed: float = 400.0 # 龟壳滑动速度
func _ready() -> void:
super._ready()
sprite.play("walk")
func _physics_process(delta: float) -> void:
if state == KoopaState.WALKING:
super._physics_process(delta)
elif state == KoopaState.SHELL_MOVING:
# 龟壳快速滑动
if not is_on_floor():
velocity += Vector2(0, gravity * delta)
velocity = Vector2(direction * shell_speed, velocity.y)
move_and_slide()
# SHELL 状态静止不动
func stomped() -> void:
if not is_alive:
return
match state:
KoopaState.WALKING:
# 第一次踩:缩进壳里
enter_shell()
GameManager.add_score(100)
KoopaState.SHELL:
# 踩静止的龟壳:开始滑动
start_shell_moving()
GameManager.add_score(200)
KoopaState.SHELL_MOVING:
# 踩滑动的龟壳:停下来
stop_shell()
GameManager.add_score(100)
## 缩进壳里
func enter_shell() -> void:
state = KoopaState.SHELL
velocity = Vector2.ZERO
sprite.play("shell")
## 龟壳开始滑动
func start_shell_moving() -> void:
state = KoopaState.SHELL_MOVING
sprite.play("shell_moving")
# 朝向踩它的人的方向滑
var player = get_tree().get_first_node_in_group("player")
if player != null:
direction = 1 if player.position.x > position.x else -1
## 龟壳停下来
func stop_shell() -> void:
state = KoopaState.SHELL
velocity = Vector2.ZERO
sprite.play("shell")
## 被侧面碰到
func side_hit() -> void:
match state:
KoopaState.SHELL:
start_shell_moving()
KoopaState.SHELL_MOVING:
stop_shell()
## 龟壳撞到其他敌人
func _on_body_entered(body: Node2D) -> void:
if state == KoopaState.SHELL_MOVING and body is Enemy and body != self:
body.die()
GameManager.add_score(200)玩家碰撞处理
在玩家脚本中添加与敌人的碰撞检测:
// 在 Player.cs 中添加
public override void _Ready()
{
// ... 其他初始化代码 ...
// 将玩家加入 "player" 组
AddToGroup("player");
}
// 检测与敌人的碰撞
private void OnEnemyAreaEntered(Area2D area)
{
var enemy = area.GetParent() as Enemy;
if (enemy == null || !enemy.IsAlive()) return;
// 判断碰撞方向
if (IsStomping(enemy))
{
// 从上方踩踏
enemy.Stomped();
BounceAfterStomp();
}
else
{
// 从侧面碰到
TakeDamage();
}
}
// 判断是否为踩踏
private bool IsStomping(Enemy enemy)
{
// 玛丽奥的底部在敌人头部的上方
// 加一个容差值避免误判
float playerBottom = GlobalPosition.Y + 8;
float enemyTop = enemy.GlobalPosition.Y - 8;
return playerBottom < enemyTop && Velocity.Y > 0;
}
// 踩踏后弹起
private void BounceAfterStomp()
{
Velocity = new Vector2(Velocity.X, -200);
}
// 受伤
private void TakeDamage()
{
if (_isInvincible) return;
if (_playerState == PlayerState.Small)
{
Die();
}
else
{
ShrinkToSmall();
}
}# 在 player.gd 中添加
func _ready() -> void:
# ... 其他初始化代码 ...
# 将玩家加入 "player" 组
add_to_group("player")
## 检测与敌人的碰撞
func _on_enemy_area_entered(area: Area2D) -> void:
var enemy = area.get_parent() as Node # Enemy
if enemy == null or not enemy.is_alive:
return
# 判断碰撞方向
if is_stomping(enemy):
# 从上方踩踏
enemy.stomped()
bounce_after_stomp()
else:
# 从侧面碰到
take_damage()
## 判断是否为踩踏
func is_stomping(enemy: Node) -> bool:
# 玛丽奥的底部在敌人头部的上方
var player_bottom: float = global_position.y + 8
var enemy_top: float = enemy.global_position.y - 8
return player_bottom < enemy_top and velocity.y > 0
## 踩踏后弹起
func bounce_after_stomp() -> void:
velocity = Vector2(velocity.x, -200)
## 受伤
func take_damage() -> void:
if is_invincible:
return
if player_state == PlayerState.SMALL:
die()
else:
shrink_to_small()下一章预告
敌人交互系统完成了!蘑菇怪可以被踩扁,乌龟可以缩壳、被踢飞。下一章我们将实现道具系统——蘑菇、火焰花、星星和金币。
