2. 项目搭建
2026/4/14大约 8 分钟
2. 雷霆战机——项目搭建
2.1 创建Godot项目
第一步:新建项目
- 打开Godot 4.x引擎
- 点击"新建项目"
- 项目名称:
Raiden - 渲染模式:2D
- 选择保存路径
- 点击"创建并编辑"
第二步:项目设置
打开 项目 → 项目设置:
| 设置项 | 值 | 说明 |
|---|---|---|
| 窗口宽度 | 360 | 竖屏射击游戏的标准宽度 |
| 窗口高度 | 640 | 竖屏射击游戏的标准高度 |
| 不可拉伸 | 勾选 | 固定窗口大小 |
| 拉伸模式 | viewport | 保持比例 |
| 拉伸方面 | keep | 保持宽高比 |
第三步:输入映射
| 动作名称 | 按键 |
|---|---|
move_up | 上方向键、W |
move_down | 下方向键、S |
move_left | 左方向键、A |
move_right | 右方向键、D |
fire | 空格(或自动射击) |
bomb | X键、B键 |
focus | Shift键(低速精确移动) |
pause | Esc、P |
2.2 目录结构
res://
├── scenes/ ← 场景文件夹
│ ├── main.tscn ← 主场景
│ ├── player.tscn ← 玩家战机
│ ├── enemies/ ← 敌人场景
│ │ ├── basic_enemy.tscn ← 基础敌人
│ │ ├── medium_enemy.tscn ← 中型敌人
│ │ ├── heavy_enemy.tscn ← 重型敌人
│ │ └── boss.tscn ← Boss
│ ├── bullets/ ← 子弹场景
│ │ ├── player_bullet.tscn ← 玩家子弹
│ │ └── enemy_bullet.tscn ← 敌人子弹
│ ├── powerups/ ← 道具场景
│ │ ├── power_up.tscn ← 武器升级
│ │ ├── bomb_pickup.tscn ← 炸弹
│ │ └── life_pickup.tscn ← 生命
│ ├── effects/ ← 特效场景
│ │ ├── explosion.tscn ← 爆炸特效
│ │ └── bomb_effect.tscn ← 炸弹特效
│ └── ui/ ← UI场景
│ ├── hud.tscn ← 游戏内UI
│ ├── pause_menu.tscn ← 暂停菜单
│ ├── game_over.tscn ← 游戏结束
│ └── main_menu.tscn ← 主菜单
├── scripts/ ← 脚本文件夹
│ ├── game_manager.gd/.cs ← 游戏管理器
│ ├── player.gd/.cs ← 玩家控制
│ ├── enemy.gd/.cs ← 敌人基类
│ ├── bullet.gd/.cs ← 子弹基类
│ ├── bullet_pool.gd/.cs ← 子弹对象池
│ ├── enemy_spawner.gd/.cs ← 敌人生成器
│ ├── powerup.gd/.cs ← 道具逻辑
│ ├── collision_manager.gd/.cs ← 碰撞管理
│ ├── wave_manager.gd/.cs ← 波次管理
│ └── constants.gd/.cs ← 常量定义
├── resources/ ← 资源文件夹
│ ├── waves/ ← 波次配置
│ └── enemy_patterns/ ← 敌人运动模式
└── assets/
├── sprites/ ← 图片素材
├── fonts/ ← 字体
└── audio/ ← 音效
├── sfx/
└── music/2.3 常量定义
C
/// <summary>
/// 雷霆战机——游戏常量
/// </summary>
public static class GameConfig
{
// ===== 画面尺寸 =====
public const int ScreenWidth = 360;
public const int ScreenHeight = 640;
// ===== 玩家参数 =====
public const float PlayerSpeed = 250f;
public const float FocusSpeedMult = 0.4f;
public const int PlayerMaxLives = 3;
public const int PlayerMaxBombs = 3;
public const float PlayerFireRate = 0.12f;
public const float PlayerBulletSpeed = 600f;
public const float PlayerHitboxRadius = 2f;
public const float InvincibleTime = 2.0f;
// ===== 子弹参数 =====
public const int BulletPoolSize = 200; // 对象池大小
public const float EnemyBulletSpeed = 150f;
public const float EnemyBulletLife = 10f; // 子弹存活时间(秒)
// ===== 敌人参数 =====
public const float EnemySpawnInterval = 1.5f;
public const float EnemyBaseSpeed = 100f;
// ===== 道具参数 =====
public const float PowerUpSpeed = 80f;
public const float PowerUpDropChance = 0.15f; // 15%掉落率
public const float PowerUpLifetime = 10f;
// ===== Boss参数 =====
public const int BossBaseHP = 200;
public const float BossEntryDuration = 3f;
// ===== 视觉效果 =====
public const float ScreenShakeOnExplosion = 3f;
public const float ScreenShakeOnBomb = 8f;
public const float ScreenShakeOnBossDeath = 12f;
}GDScript
## 雷霆战机——游戏常量
class_name GameConfig
# ===== 画面尺寸 =====
const SCREEN_WIDTH: int = 360
const SCREEN_HEIGHT: int = 640
# ===== 玩家参数 =====
const PLAYER_SPEED: float = 250.0
const FOCUS_SPEED_MULT: float = 0.4
const PLAYER_MAX_LIVES: int = 3
const PLAYER_MAX_BOMBS: int = 3
const PLAYER_FIRE_RATE: float = 0.12
const PLAYER_BULLET_SPEED: float = 600.0
const PLAYER_HITBOX_RADIUS: float = 2.0
const INVINCIBLE_TIME: float = 2.0
# ===== 子弹参数 =====
const BULLET_POOL_SIZE: int = 200
const ENEMY_BULLET_SPEED: float = 150.0
const ENEMY_BULLET_LIFE: float = 10.0
# ===== 敌人参数 =====
const ENEMY_SPAWN_INTERVAL: float = 1.5
const ENEMY_BASE_SPEED: float = 100.0
# ===== 道具参数 =====
const POWERUP_SPEED: float = 80.0
const POWERUP_DROP_CHANCE: float = 0.15
const POWERUP_LIFETIME: float = 10.0
# ===== Boss参数 =====
const BOSS_BASE_HP: int = 200
const BOSS_ENTRY_DURATION: float = 3.0
# ===== 视觉效果 =====
const SCREEN_SHAKE_ON_EXPLOSION: float = 3.0
const SCREEN_SHAKE_ON_BOMB: float = 8.0
const SCREEN_SHAKE_ON_BOSS_DEATH: float = 12.02.4 主场景结构
主场景是整个游戏的容器:
Main (Node)
├── Background (ParallaxBackground) ← 背景滚动
│ └── Starfield (ParallaxLayer) ← 星空背景
├── GameArea (Node2D) ← 游戏区域容器
│ ├── Player (Area2D) ← 玩家
│ ├── Enemies (Node2D) ← 敌人容器
│ ├── PlayerBullets (Node2D) ← 玩家子弹容器
│ ├── EnemyBullets (Node2D) ← 敌人子弹容器
│ ├── PowerUps (Node2D) ← 道具容器
│ └── Effects (Node2D) ← 特效容器
├── CollisionManager (Node) ← 碰撞管理
├── WaveManager (Node) ← 波次管理
├── HUD (CanvasLayer) ← 游戏UI
├── PauseMenu (CanvasLayer) ← 暂停菜单
├── GameOverScreen (CanvasLayer) ← 游戏结束
├── MainMenu (CanvasLayer) ← 主菜单
└── AudioManager (Node) ← 音效管理2.5 滚动背景
纵版射击游戏需要一个不断向下滚动的背景,营造飞机在向前飞行的感觉。我们使用Godot的 ParallaxBackground(视差背景)来实现。
C
using Godot;
/// <summary>
/// 星空滚动背景
/// </summary>
public partial class StarfieldBackground : ParallaxLayer
{
private Sprite2D _starsSprite;
// 滚动速度(越大越快)
[Export] public float ScrollSpeed { get; set; } = 50f;
public override void _Ready()
{
_starsSprite = GetNode<Sprite2D>("Stars");
// 设置视差层的运动缩放
MotionMirroring = new Vector2(0, -GetViewportRect().Size.Y);
}
public override void _Process(double delta)
{
// 通过偏移实现滚动
MotionOffset += new Vector2(0, ScrollSpeed * (float)delta);
}
}GDScript
extends ParallaxLayer
## 星空滚动背景
## 滚动速度(越大越快)
@export var scroll_speed: float = 50.0
func _ready() -> void:
# 设置视差层的运动缩放
motion_mirroring = Vector2(0, -get_viewport_rect().size.y)
func _process(delta: float) -> void:
# 通过偏移实现滚动
motion_offset += Vector2(0, scroll_speed * delta)2.6 GameManager脚本
C
using Godot;
/// <summary>
/// 雷霆战机游戏管理器
/// </summary>
public partial class RaidenGameManager : Node
{
// ===== 信号 =====
[Signal] public delegate void ScoreChangedEventHandler(int score);
[Signal] public delegate void LivesChangedEventHandler(int lives);
[Signal] public delegate void BombsChangedEventHandler(int bombs);
[Signal] public delegate void WeaponChangedEventHandler(int level);
[Signal] public delegate void StateChangedEventHandler(int state);
// ===== 游戏状态 =====
public enum GameState { Menu, Playing, Paused, GameOver }
private GameState _currentState = GameState.Menu;
// ===== 游戏数据 =====
private int _score = 0;
private int _lives = 3;
private int _bombs = 3;
private int _weaponLevel = 1;
private int _highScore = 0;
// ===== 属性 =====
public int Score => _score;
public int Lives => _lives;
public int Bombs => _bombs;
public int WeaponLevel => _weaponLevel;
public int HighScore => _highScore;
public GameState CurrentState
{
get => _currentState;
set => SetState(value);
}
public override void _Ready()
{
// 加载最高分
_highScore = LoadHighScore();
}
public void StartGame()
{
_score = 0;
_lives = GameConfig.PlayerMaxLives;
_bombs = GameConfig.PlayerMaxBombs;
_weaponLevel = 1;
SetState(GameState.Playing);
EmitSignal(SignalName.ScoreChanged, _score);
EmitSignal(SignalName.LivesChanged, _lives);
EmitSignal(SignalName.BombsChanged, _bombs);
EmitSignal(SignalName.WeaponChanged, _weaponLevel);
}
public void AddScore(int points)
{
_score += points;
EmitSignal(SignalName.ScoreChanged, _score);
}
public void LoseLife()
{
_lives--;
_weaponLevel = Mathf.Max(1, _weaponLevel - 1);
EmitSignal(SignalName.LivesChanged, _lives);
EmitSignal(SignalName.WeaponChanged, _weaponLevel);
if (_lives <= 0)
{
// 保存最高分
if (_score > _highScore)
{
_highScore = _score;
SaveHighScore(_highScore);
}
SetState(GameState.GameOver);
}
}
public void UseBomb()
{
if (_bombs <= 0) return;
_bombs--;
EmitSignal(SignalName.BombsChanged, _bombs);
}
public void CollectBomb()
{
_bombs = Mathf.Min(_bombs + 1, 5);
EmitSignal(SignalName.BombsChanged, _bombs);
}
public void UpgradeWeapon()
{
if (_weaponLevel < 5)
{
_weaponLevel++;
EmitSignal(SignalName.WeaponChanged, _weaponLevel);
}
}
public void TogglePause()
{
if (_currentState == GameState.Playing)
SetState(GameState.Paused);
else if (_currentState == GameState.Paused)
SetState(GameState.Playing);
}
private void SetState(GameState newState)
{
_currentState = newState;
GetTree().Paused = (newState == GameState.Paused);
EmitSignal(SignalName.StateChanged, (int)newState);
}
// ===== 存档 =====
private int LoadHighScore()
{
var config = new ConfigFile();
config.Load("user://raiden_save.cfg");
return (int)config.GetValue("stats", "high_score", 0);
}
private void SaveHighScore(int score)
{
var config = new ConfigFile();
config.Load("user://raiden_save.cfg");
config.SetValue("stats", "high_score", score);
config.Save("user://raiden_save.cfg");
}
}GDScript
extends Node
## 雷霆战机游戏管理器
# ===== 信号 =====
signal score_changed(score: int)
signal lives_changed(lives: int)
signal bombs_changed(bombs: int)
signal weapon_changed(level: int)
signal state_changed(state: int)
# ===== 游戏状态 =====
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var _current_state: GameState = GameState.MENU
# ===== 游戏数据 =====
var _score: int = 0
var _lives: int = 3
var _bombs: int = 3
var _weapon_level: int = 1
var _high_score: int = 0
# ===== 属性 =====
var score: int:
get: return _score
var lives: int:
get: return _lives
var bombs: int:
get: return _bombs
var weapon_level: int:
get: return _weapon_level
var high_score: int:
get: return _high_score
func _ready() -> void:
_high_score = _load_high_score()
func start_game() -> void:
_score = 0
_lives = GameConfig.PLAYER_MAX_LIVES
_bombs = GameConfig.PLAYER_MAX_BOMBS
_weapon_level = 1
_set_state(GameState.PLAYING)
score_changed.emit(_score)
lives_changed.emit(_lives)
bombs_changed.emit(_bombs)
weapon_changed.emit(_weapon_level)
func add_score(points: int) -> void:
_score += points
score_changed.emit(_score)
func lose_life() -> void:
_lives -= 1
_weapon_level = maxi(1, _weapon_level - 1)
lives_changed.emit(_lives)
weapon_changed.emit(_weapon_level)
if _lives <= 0:
if _score > _high_score:
_high_score = _score
_save_high_score(_high_score)
_set_state(GameState.GAME_OVER)
func use_bomb() -> void:
if _bombs <= 0:
return
_bombs -= 1
bombs_changed.emit(_bombs)
func collect_bomb() -> void:
_bombs = mini(_bombs + 1, 5)
bombs_changed.emit(_bombs)
func upgrade_weapon() -> void:
if _weapon_level < 5:
_weapon_level += 1
weapon_changed.emit(_weapon_level)
func toggle_pause() -> void:
if _current_state == GameState.PLAYING:
_set_state(GameState.PAUSED)
elif _current_state == GameState.PAUSED:
_set_state(GameState.PLAYING)
func _set_state(new_state: GameState) -> void:
_current_state = new_state
get_tree().paused = (new_state == GameState.PAUSED)
state_changed.emit(new_state)
func _load_high_score() -> int:
var config = ConfigFile.new()
config.load("user://raiden_save.cfg")
return config.get_value("stats", "high_score", 0)
func _save_high_score(sc: int) -> void:
var config = ConfigFile.new()
config.load("user://raiden_save.cfg")
config.set_value("stats", "high_score", sc)
config.save("user://raiden_save.cfg")2.7 碰撞管理器
射击游戏中碰撞检测非常多——需要管理玩家子弹vs敌人、敌人子弹vs玩家、玩家vs道具等。我们用一个碰撞管理器统一处理。
C
using Godot;
using System.Collections.Generic;
/// <summary>
/// 碰撞管理器——统一处理所有碰撞检测
/// </summary>
public partial class CollisionManager : Node
{
// 引用各容器节点
private Node2D _playerBullets;
private Node2D _enemyBullets;
private Node2D _enemies;
private Node2D _powerUps;
private Area2D _player;
public override void _Ready()
{
_playerBullets = GetNode<Node2D>("../GameArea/PlayerBullets");
_enemyBullets = GetNode<Node2D>("../GameArea/EnemyBullets");
_enemies = GetNode<Node2D>("../GameArea/Enemies");
_powerUps = GetNode<Node2D>("../GameArea/PowerUps");
_player = GetNode<Area2D>("../GameArea/Player");
}
/// <summary>
/// 每帧检测碰撞
/// </summary>
public override void _PhysicsProcess(double delta)
{
CheckPlayerBulletsVsEnemies();
CheckEnemyBulletsVsPlayer();
CheckPlayerVsPowerUps();
}
/// <summary>
/// 玩家子弹 vs 敌人
/// </summary>
private void CheckPlayerBulletsVsEnemies()
{
foreach (var bullet in _playerBullets.GetChildren())
{
var b = bullet as Area2D;
if (b == null) continue;
foreach (var enemy in _enemies.GetChildren())
{
var e = enemy as Area2D;
if (e == null) continue;
if (IsOverlapping(b, e))
{
// 子弹命中敌人
e.Call("TakeDamage", 1);
b.Call("Destroy");
break;
}
}
}
}
/// <summary>
/// 敌人子弹 vs 玩家
/// </summary>
private void CheckEnemyBulletsVsPlayer()
{
if (_player.Call("IsInvincible").AsBool()) return;
foreach (var bullet in _enemyBullets.GetChildren())
{
var b = bullet as Area2D;
if (b == null) continue;
// 用判定点半径检测,不用整个飞机的碰撞体
float dist = _player.GlobalPosition.DistanceTo(b.GlobalPosition);
if (dist < GameConfig.PlayerHitboxRadius + 4f)
{
_player.Call("OnHit");
b.Call("Destroy");
break;
}
}
}
/// <summary>
/// 玩家 vs 道具
/// </summary>
private void CheckPlayerVsPowerUps()
{
foreach (var powerup in _powerUps.GetChildren())
{
var p = powerup as Area2D;
if (p == null) continue;
float dist = _player.GlobalPosition.DistanceTo(p.GlobalPosition);
if (dist < 20f) // 道具拾取范围比子弹大
{
p.Call("Collect");
}
}
}
/// <summary>
/// 简单的圆形碰撞检测
/// </summary>
private bool IsOverlapping(Area2D a, Area2D b)
{
float dist = a.GlobalPosition.DistanceTo(b.GlobalPosition);
return dist < 15f; // 碰撞距离
}
}GDScript
extends Node
## 碰撞管理器——统一处理所有碰撞检测
var _player_bullets: Node2D
var _enemy_bullets: Node2D
var _enemies: Node2D
var _power_ups: Node2D
var _player: Area2D
func _ready() -> void:
_player_bullets = get_node("../GameArea/PlayerBullets")
_enemy_bullets = get_node("../GameArea/EnemyBullets")
_enemies = get_node("../GameArea/Enemies")
_power_ups = get_node("../GameArea/PowerUps")
_player = get_node("../GameArea/Player")
func _physics_process(delta: float) -> void:
_check_player_bullets_vs_enemies()
_check_enemy_bullets_vs_player()
_check_player_vs_power_ups()
## 玩家子弹 vs 敌人
func _check_player_bullets_vs_enemies() -> void:
for bullet in _player_bullets.get_children():
var b = bullet as Area2D
if b == null:
continue
for enemy in _enemies.get_children():
var e = enemy as Area2D
if e == null:
continue
if _is_overlapping(b, e):
e.take_damage(1)
b.destroy()
break
## 敌人子弹 vs 玩家
func _check_enemy_bullets_vs_player() -> void:
if _player.is_invincible():
return
for bullet in _enemy_bullets.get_children():
var b = bullet as Area2D
if b == null:
continue
var dist = _player.global_position.distance_to(b.global_position)
if dist < GameConfig.PLAYER_HITBOX_RADIUS + 4.0:
_player.on_hit()
b.destroy()
break
## 玩家 vs 道具
func _check_player_vs_power_ups() -> void:
for powerup in _power_ups.get_children():
var p = powerup as Area2D
if p == null:
continue
var dist = _player.global_position.distance_to(p.global_position)
if dist < 20.0:
p.collect()
## 简单的圆形碰撞检测
func _is_overlapping(a: Area2D, b: Area2D) -> bool:
var dist = a.global_position.distance_to(b.global_position)
return dist < 15.02.8 本章小结
| 内容 | 说明 |
|---|---|
| 项目创建 | 360x640竖屏2D项目 |
| 目录结构 | 按功能模块组织 |
| 常量定义 | 统一管理所有游戏参数 |
| 主场景 | 包含所有游戏子系统的层级结构 |
| 滚动背景 | ParallaxLayer实现星空滚动 |
| GameManager | 全局状态和数据管理 |
| 碰撞管理 | 统一处理所有碰撞检测 |
下一章我们将实现玩家移动和射击系统。
