mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-22 10:34:30 +08:00
- 添加简体中文本地化配置和导航链接 - 配置本地搜索功能并设置中文翻译 - 添加 Catch-all 404 重定向中间件 - 重构侧边栏和导航结构以支持国际化 - 移除旧的英文文档内容并更新配置 - 添加页脚导航和面包屑文字本地化
1624 lines
44 KiB
Markdown
1624 lines
44 KiB
Markdown
# 从零开始 - GFramework 游戏开发教程
|
||
|
||
> 这是一个完整的从零开始的教程,将带领你创建一个使用 GFramework 的简单游戏项目。
|
||
|
||
## 📋 目录
|
||
|
||
- [环境准备](#环境准备)
|
||
- [项目创建](#项目创建)
|
||
- [架构设计](#架构设计)
|
||
- [功能实现](#功能实现)
|
||
- [测试验证](#测试验证)
|
||
- [项目打包](#项目打包)
|
||
- [进阶功能](#进阶功能)
|
||
|
||
## 环境准备
|
||
|
||
### 系统要求
|
||
|
||
- **操作系统**: Windows 10+, macOS 10.15+, 或 Linux
|
||
- **.NET SDK**: 6.0 或更高版本
|
||
- **Godot 引擎**: 4.5.1 或更高版本
|
||
- **IDE**: Visual Studio 2022+, JetBrains Rider, 或 VS Code
|
||
|
||
### 安装 .NET SDK
|
||
|
||
1. 访问 [.NET 官网](https://dotnet.microsoft.com/download)
|
||
2. 下载并安装 .NET 6.0 SDK
|
||
3. 验证安装:
|
||
|
||
```bash
|
||
dotnet --version
|
||
# 应该显示 6.0.x 或更高版本
|
||
```
|
||
|
||
### 安装 Godot
|
||
|
||
1. 访问 [Godot 官网](https://godotengine.org/download)
|
||
2. 下载 Godot 4.5.1
|
||
3. 解压到合适的位置并启动
|
||
4. 在编辑器设置中确认 .NET 支持
|
||
|
||
### 验证环境
|
||
|
||
创建一个测试项目验证环境:
|
||
|
||
```bash
|
||
# 创建测试项目
|
||
dotnet new console -n TestProject
|
||
cd TestProject
|
||
|
||
# 如果使用 Godot,添加 Godot 引用
|
||
dotnet add package GeWuYou.GFramework.Core
|
||
dotnet add package GeWuYou.GFramework.Godot
|
||
|
||
# 编译测试
|
||
dotnet build
|
||
```
|
||
|
||
## 项目创建
|
||
|
||
### 1. 创建新的 Godot 项目
|
||
|
||
1. 打开 Godot 编辑器
|
||
2. 点击 "新建项目"
|
||
3. 创建项目文件夹,命名为 "MyGFrameworkGame"
|
||
4. 选择 C# 作为脚本语言
|
||
|
||
### 2. 配置项目结构
|
||
|
||
在项目根目录创建以下文件夹结构:
|
||
|
||
```
|
||
MyGFrameworkGame/
|
||
├── src/
|
||
│ ├── Game/ # 游戏逻辑
|
||
│ │ ├── Models/ # 数据模型
|
||
│ │ ├── Systems/ # 业务系统
|
||
│ │ ├── Controllers/ # 控制器
|
||
│ │ └── Utilities/ # 工具类
|
||
│ └── Game.Core/ # 核心游戏组件
|
||
├── assets/ # 游戏资源
|
||
│ ├── scenes/
|
||
│ ├── textures/
|
||
│ ├── audio/
|
||
│ └── ui/
|
||
└── project.godot # Godot 项目文件
|
||
```
|
||
|
||
### 3. 配置项目文件
|
||
|
||
创建 `src/Game/Game.csproj`:
|
||
|
||
```xml
|
||
<Project Sdk="Microsoft.NET.Sdk">
|
||
|
||
<PropertyGroup>
|
||
<TargetFramework>net6.0</TargetFramework>
|
||
<RootNamespace>MyGFrameworkGame</RootNamespace>
|
||
<Nullable>enable</Nullable>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
<ProjectReference Include="..\Game.Core\Game.Core.csproj" />
|
||
</ItemGroup>
|
||
|
||
<ItemGroup>
|
||
<PackageReference Include="GeWuYou.GFramework.Core" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.Godot" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.SourceGenerators" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.SourceGenerators.Attributes" Version="1.0.0" />
|
||
</ItemGroup>
|
||
|
||
</Project>
|
||
```
|
||
|
||
创建 `src/Game.Core/Game.Core.csproj`:
|
||
|
||
```xml
|
||
<Project Sdk="Microsoft.NET.Sdk">
|
||
|
||
<PropertyGroup>
|
||
<TargetFramework>net6.0</TargetFramework>
|
||
<RootNamespace>MyGFrameworkGame.Core</RootNamespace>
|
||
<Nullable>enable</Nullable>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
<PackageReference Include="GeWuYou.GFramework.Core" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.Core.Abstractions" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.Game" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.Game.Abstractions" Version="1.0.0" />
|
||
</ItemGroup>
|
||
|
||
</Project>
|
||
```
|
||
|
||
### 4. 配置 Godot 项目
|
||
|
||
在 `project.godot` 中添加 C# 配置:
|
||
|
||
```ini
|
||
; Engine configuration file.
|
||
; It's best edited using the editor UI and not directly,
|
||
; since the parameters that go here are not all obvious.
|
||
;
|
||
; Format:
|
||
; [section] ; section goes between []
|
||
; param=value ; assign values to parameters
|
||
|
||
[application]
|
||
|
||
config/name="My GFramework Game"
|
||
config/description="A game built with GFramework"
|
||
run/main_scene="res://src/Game/MainScene.tscn"
|
||
config/features=PackedStringArray("4.5", "Forward Plus")
|
||
|
||
[dotnet]
|
||
|
||
project/assembly_name="MyGFrameworkGame"
|
||
project/solution_directory="src/"
|
||
|
||
```
|
||
|
||
## 架构设计
|
||
|
||
### 1. 定义游戏架构
|
||
|
||
创建 `src/Game.Core/Architecture/GameArchitecture.cs`:
|
||
|
||
```csharp
|
||
using GFramework.Core.architecture;
|
||
using MyGFrameworkGame.Core.Models;
|
||
using MyGFrameworkGame.Core.Systems;
|
||
using MyGFrameworkGame.Core.Utilities;
|
||
|
||
namespace MyGFrameworkGame.Core.Architecture
|
||
{
|
||
public class GameArchitecture : AbstractArchitecture
|
||
{
|
||
protected override void Init()
|
||
{
|
||
// 注册游戏模型
|
||
RegisterModel(new PlayerModel());
|
||
RegisterModel(new GameModel());
|
||
RegisterModel(new ScoreModel());
|
||
|
||
// 注册游戏系统
|
||
RegisterSystem(new PlayerControllerSystem());
|
||
RegisterSystem(new EnemySpawnerSystem());
|
||
RegisterSystem(new CollisionSystem());
|
||
RegisterSystem(new ScoreSystem());
|
||
|
||
// 注册工具类
|
||
RegisterUtility(new StorageUtility());
|
||
RegisterUtility(new AudioUtility());
|
||
RegisterUtility(new AssetLoadUtility());
|
||
}
|
||
|
||
protected override void InstallModules()
|
||
{
|
||
// 如果需要 Godot 特定模块
|
||
// InstallGodotModule(new AudioModule());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 创建核心模型
|
||
|
||
创建 `src/Game.Core/Models/PlayerModel.cs`:
|
||
|
||
```csharp
|
||
using GFramework.Core.model;
|
||
|
||
namespace MyGFrameworkGame.Core.Models
|
||
{
|
||
public class PlayerModel : AbstractModel
|
||
{
|
||
public BindableProperty<int> Health { get; } = new(100);
|
||
public BindableProperty<int> MaxHealth { get; } = new(100);
|
||
public BindableProperty<float> Speed { get; } = new(300.0f);
|
||
public BindableProperty<int> Score { get; } = new(0);
|
||
public BindableProperty<bool> IsAlive { get; } = new(true);
|
||
public BindableProperty<bool> CanShoot { get; } = new(true);
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// 监听生命值变化
|
||
Health.Register(health => {
|
||
if (health <= 0)
|
||
{
|
||
IsAlive.Value = false;
|
||
SendEvent(new PlayerDeathEvent());
|
||
}
|
||
});
|
||
|
||
// 监听分数变化
|
||
Score.Register(score => {
|
||
if (score % 100 == 0)
|
||
{
|
||
SendEvent(new MilestoneEvent { Score = score });
|
||
}
|
||
});
|
||
}
|
||
|
||
public void TakeDamage(int damage)
|
||
{
|
||
if (IsAlive.Value)
|
||
{
|
||
var newHealth = Math.Max(0, Health.Value - damage);
|
||
Health.Value = newHealth;
|
||
}
|
||
}
|
||
|
||
public void Heal(int amount)
|
||
{
|
||
var newHealth = Math.Min(MaxHealth.Value, Health.Value + amount);
|
||
Health.Value = newHealth;
|
||
}
|
||
|
||
public bool CanShoot()
|
||
{
|
||
return IsAlive.Value && CanShoot.Value;
|
||
}
|
||
}
|
||
|
||
// 游戏事件
|
||
public struct PlayerDeathEvent { }
|
||
public struct MilestoneEvent { public int Score; }
|
||
}
|
||
```
|
||
|
||
创建 `src/Game.Core/Models/GameModel.cs`:
|
||
|
||
```csharp
|
||
using GFramework.Core.model;
|
||
|
||
namespace MyGFrameworkGame.Core.Models
|
||
{
|
||
public class GameModel : AbstractModel
|
||
{
|
||
public BindableProperty<GameState> State { get; } = new(GameState.Menu);
|
||
public BindableProperty<int> CurrentLevel { get; } = new(1);
|
||
public BindableProperty<int> EnemyCount { get; } = new(0);
|
||
public BindableProperty<float> GameTime { get; } = new(0.0f);
|
||
public BindableProperty<bool> IsPaused { get; } = new(false);
|
||
|
||
protected override void OnInit()
|
||
{
|
||
State.Register(state => {
|
||
switch (state)
|
||
{
|
||
case GameState.Playing:
|
||
SendEvent(new GameStartEvent());
|
||
break;
|
||
case GameState.Paused:
|
||
SendEvent(new GamePauseEvent());
|
||
break;
|
||
case GameState.GameOver:
|
||
SendEvent(new GameOverEvent());
|
||
break;
|
||
case GameState.Menu:
|
||
SendEvent(new GameMenuEvent());
|
||
break;
|
||
}
|
||
});
|
||
}
|
||
|
||
public void StartGame()
|
||
{
|
||
State.Value = GameState.Playing;
|
||
EnemyCount.Value = 0;
|
||
GameTime.Value = 0.0f;
|
||
}
|
||
|
||
public void PauseGame()
|
||
{
|
||
if (State.Value == GameState.Playing)
|
||
{
|
||
State.Value = GameState.Paused;
|
||
}
|
||
}
|
||
|
||
public void ResumeGame()
|
||
{
|
||
if (State.Value == GameState.Paused)
|
||
{
|
||
State.Value = GameState.Playing;
|
||
}
|
||
}
|
||
|
||
public void GameOver()
|
||
{
|
||
State.Value = GameState.GameOver;
|
||
}
|
||
|
||
public void ReturnToMenu()
|
||
{
|
||
State.Value = GameState.Menu;
|
||
}
|
||
}
|
||
|
||
public enum GameState
|
||
{
|
||
Menu,
|
||
Playing,
|
||
Paused,
|
||
GameOver
|
||
}
|
||
|
||
// 游戏事件
|
||
public struct GameStartEvent { }
|
||
public struct GamePauseEvent { }
|
||
public struct GameResumeEvent { }
|
||
public struct GameOverEvent { }
|
||
public struct GameMenuEvent { }
|
||
}
|
||
```
|
||
|
||
### 3. 创建核心系统
|
||
|
||
创建 `src/Game.Core/Systems/PlayerControllerSystem.cs`:
|
||
|
||
```csharp
|
||
using Godot;
|
||
using GFramework.Core.system;
|
||
using GFramework.Core.events;
|
||
using MyGFrameworkGame.Core.Models;
|
||
|
||
namespace MyGFrameworkGame.Core.Systems
|
||
{
|
||
public class PlayerControllerSystem : AbstractSystem
|
||
{
|
||
private PlayerModel _playerModel;
|
||
private GameModel _gameModel;
|
||
|
||
protected override void OnInit()
|
||
{
|
||
_playerModel = GetModel<PlayerModel>();
|
||
_gameModel = GetModel<GameModel>();
|
||
|
||
// 监听输入事件
|
||
RegisterEvent<PlayerInputEvent>(OnPlayerInput);
|
||
RegisterEvent<GameStartEvent>(OnGameStart);
|
||
RegisterEvent<GameOverEvent>(OnGameOver);
|
||
}
|
||
|
||
private void OnPlayerInput(PlayerInputEvent inputEvent)
|
||
{
|
||
if (_gameModel.State.Value != GameState.Playing)
|
||
return;
|
||
|
||
switch (inputEvent.Action)
|
||
{
|
||
case "move":
|
||
HandleMovement(inputEvent.Direction);
|
||
break;
|
||
case "shoot":
|
||
HandleShoot();
|
||
break;
|
||
case "pause":
|
||
_gameModel.PauseGame();
|
||
break;
|
||
}
|
||
}
|
||
|
||
private void HandleMovement(Vector2 direction)
|
||
{
|
||
if (!_playerModel.IsAlive.Value)
|
||
return;
|
||
|
||
// 发送移动事件,由 Godot 控制器处理
|
||
SendEvent(new PlayerMoveEvent { Direction = direction.Normalized() * _playerModel.Speed.Value });
|
||
}
|
||
|
||
private void HandleShoot()
|
||
{
|
||
if (!_playerModel.CanShoot())
|
||
return;
|
||
|
||
// 发送射击事件
|
||
SendEvent(new PlayerShootEvent());
|
||
_playerModel.CanShoot.Value = false;
|
||
|
||
// 重置射击冷却
|
||
this.StartCoroutine(ShootCooldown());
|
||
}
|
||
|
||
private System.Collections.IEnumerator ShootCooldown()
|
||
{
|
||
yield return new WaitForSeconds(0.3f);
|
||
_playerModel.CanShoot.Value = true;
|
||
}
|
||
|
||
private void OnGameStart(GameStartEvent e)
|
||
{
|
||
// 重置玩家状态
|
||
_playerModel.Health.Value = _playerModel.MaxHealth.Value;
|
||
_playerModel.Score.Value = 0;
|
||
_playerModel.IsAlive.Value = true;
|
||
_playerModel.CanShoot.Value = true;
|
||
}
|
||
|
||
private void OnGameOver(GameOverEvent e)
|
||
{
|
||
// 游戏结束时的处理
|
||
SendEvent(new SaveScoreEvent { Score = _playerModel.Score.Value });
|
||
}
|
||
}
|
||
|
||
// 输入事件
|
||
public struct PlayerInputEvent
|
||
{
|
||
public string Action { get; set; }
|
||
public Vector2 Direction { get; set; }
|
||
}
|
||
|
||
// 移动事件
|
||
public struct PlayerMoveEvent
|
||
{
|
||
public Vector2 Direction { get; set; }
|
||
}
|
||
|
||
// 射击事件
|
||
public struct PlayerShootEvent { }
|
||
|
||
// 保存分数事件
|
||
public struct SaveScoreEvent { public int Score; }
|
||
|
||
// 协程等待类(简化版)
|
||
public class WaitForSeconds
|
||
{
|
||
public float Seconds { get; }
|
||
public WaitForSeconds(float seconds) => Seconds = seconds;
|
||
}
|
||
}
|
||
```
|
||
|
||
## 功能实现
|
||
|
||
### 1. 创建主场景
|
||
|
||
创建 `src/Game/MainScene.cs`:
|
||
|
||
```csharp
|
||
using Godot;
|
||
using GFramework.Godot.extensions;
|
||
using GFramework.Godot.architecture;
|
||
using MyGFrameworkGame.Core.Architecture;
|
||
using MyGFrameworkGame.Core.Models;
|
||
using MyGFrameworkGame.Core.Systems;
|
||
|
||
namespace MyGFrameworkGame
|
||
{
|
||
[ContextAware]
|
||
[Log]
|
||
public partial class MainScene : Node2D
|
||
{
|
||
private GameArchitecture _architecture;
|
||
private Player _player;
|
||
private UI _ui;
|
||
private EnemySpawner _enemySpawner;
|
||
|
||
public override void _Ready()
|
||
{
|
||
Logger.Info("Main scene ready");
|
||
|
||
// 初始化架构
|
||
InitializeArchitecture();
|
||
|
||
// 创建游戏对象
|
||
CreateGameObjects();
|
||
|
||
// 注册事件监听
|
||
RegisterEventListeners();
|
||
|
||
// 开始游戏
|
||
StartGame();
|
||
}
|
||
|
||
private void InitializeArchitecture()
|
||
{
|
||
Logger.Info("Initializing game architecture");
|
||
|
||
_architecture = new GameArchitecture();
|
||
_architecture.Initialize();
|
||
|
||
// 设置上下文
|
||
SetContext(_architecture.Context);
|
||
}
|
||
|
||
private void CreateGameObjects()
|
||
{
|
||
Logger.Info("Creating game objects");
|
||
|
||
// 创建玩家
|
||
var playerScene = GD.Load<PackedScene>("res://assets/scenes/Player.tscn");
|
||
_player = playerScene.Instantiate<Player>();
|
||
AddChild(_player);
|
||
|
||
// 创建 UI
|
||
var uiScene = GD.Load<PackedScene>("res://assets/scenes/UI.tscn");
|
||
_ui = uiScene.Instantiate<UI>();
|
||
AddChild(_ui);
|
||
|
||
// 创建敌人生成器
|
||
_enemySpawner = new EnemySpawner();
|
||
AddChild(_enemySpawner);
|
||
}
|
||
|
||
private void RegisterEventListeners()
|
||
{
|
||
Logger.Info("Registering event listeners");
|
||
|
||
// 游戏状态事件
|
||
this.RegisterEvent<GameStartEvent>(OnGameStart)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
this.RegisterEvent<GameOverEvent>(OnGameOver)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
this.RegisterEvent<GamePauseEvent>(OnGamePause)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
// 玩家事件
|
||
this.RegisterEvent<PlayerDeathEvent>(OnPlayerDeath)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
// 分数事件
|
||
this.RegisterEvent<SaveScoreEvent>(OnSaveScore)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
|
||
private void StartGame()
|
||
{
|
||
Logger.Info("Starting game");
|
||
var gameModel = Context.GetModel<GameModel>();
|
||
gameModel.StartGame();
|
||
}
|
||
|
||
private void OnGameStart(GameStartEvent e)
|
||
{
|
||
Logger.Info("Game started");
|
||
_ui.ShowGameplayUI();
|
||
}
|
||
|
||
private void OnGameOver(GameOverEvent e)
|
||
{
|
||
Logger.Info("Game over");
|
||
_ui.ShowGameOverScreen();
|
||
}
|
||
|
||
private void OnGamePause(GamePauseEvent e)
|
||
{
|
||
Logger.Info("Game paused");
|
||
_ui.ShowPauseMenu();
|
||
GetTree().Paused = true;
|
||
}
|
||
|
||
private void OnPlayerDeath(PlayerDeathEvent e)
|
||
{
|
||
Logger.Info("Player died");
|
||
var gameModel = Context.GetModel<GameModel>();
|
||
gameModel.GameOver();
|
||
}
|
||
|
||
private void OnSaveScore(SaveScoreEvent e)
|
||
{
|
||
Logger.Info($"Saving score: {e.Score}");
|
||
_ui.UpdateFinalScore(e.Score);
|
||
}
|
||
|
||
public override void _Input(InputEvent @event)
|
||
{
|
||
// 处理退出游戏
|
||
if (@event.IsActionPressed("ui_cancel"))
|
||
{
|
||
var gameModel = Context.GetModel<GameModel>();
|
||
if (gameModel.State.Value == GameState.Playing)
|
||
{
|
||
gameModel.PauseGame();
|
||
}
|
||
else if (gameModel.State.Value == GameState.Paused)
|
||
{
|
||
gameModel.ResumeGame();
|
||
GetTree().Paused = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
protected override void Dispose(bool disposing)
|
||
{
|
||
if (disposing)
|
||
{
|
||
_architecture?.Destroy();
|
||
}
|
||
base.Dispose(disposing);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 创建玩家控制器
|
||
|
||
创建 `src/Game/Player.cs`:
|
||
|
||
```csharp
|
||
using Godot;
|
||
using GFramework.Godot.extensions;
|
||
using GFramework.Core.events;
|
||
using MyGFrameworkGame.Core.Models;
|
||
using MyGFrameworkGame.Core.Systems;
|
||
|
||
namespace MyGFrameworkGame
|
||
{
|
||
[ContextAware]
|
||
[Log]
|
||
public partial class Player : CharacterBody2D
|
||
{
|
||
[Export] public float Speed { get; set; } = 300.0f;
|
||
|
||
private PlayerModel _playerModel;
|
||
private GameModel _gameModel;
|
||
private AnimatedSprite2D _animatedSprite;
|
||
private CollisionShape2D _collisionShape;
|
||
private Timer _invincibilityTimer;
|
||
|
||
public override void _Ready()
|
||
{
|
||
Logger.Info("Player ready");
|
||
|
||
// 获取组件引用
|
||
_animatedSprite = GetNode<AnimatedSprite2D>("AnimatedSprite2D");
|
||
_collisionShape = GetNode<CollisionShape2D>("CollisionShape2D");
|
||
_invincibilityTimer = GetNode<Timer>("InvincibilityTimer");
|
||
|
||
// 设置上下文
|
||
_playerModel = Context.GetModel<PlayerModel>();
|
||
_gameModel = Context.GetModel<GameModel>();
|
||
|
||
// 注册事件监听
|
||
RegisterEventListeners();
|
||
|
||
// 设置动画
|
||
_animatedSprite.Play("idle");
|
||
}
|
||
|
||
private void RegisterEventListeners()
|
||
{
|
||
// 移动事件
|
||
this.RegisterEvent<PlayerMoveEvent>(OnPlayerMove)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
// 射击事件
|
||
this.RegisterEvent<PlayerShootEvent>(OnPlayerShoot)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
// 玩家状态变化
|
||
_playerModel.Health.Register(OnHealthChanged)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
_playerModel.IsAlive.Register(OnAliveChanged)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
|
||
public override void _Process(double delta)
|
||
{
|
||
// 处理输入并发送事件
|
||
HandleInput();
|
||
|
||
// 更新位置
|
||
var collision = MoveAndCollide(Velocity * (float)delta);
|
||
if (collision != null)
|
||
{
|
||
HandleCollision(collision);
|
||
}
|
||
}
|
||
|
||
private void HandleInput()
|
||
{
|
||
var inputVector = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
|
||
|
||
if (inputVector != Vector2.Zero)
|
||
{
|
||
// 发送移动事件
|
||
SendEvent(new PlayerInputEvent
|
||
{
|
||
Action = "move",
|
||
Direction = inputVector
|
||
});
|
||
|
||
// 更新动画
|
||
UpdateAnimation(inputVector);
|
||
}
|
||
|
||
// 射击输入
|
||
if (Input.IsActionJustPressed("shoot"))
|
||
{
|
||
SendEvent(new PlayerInputEvent { Action = "shoot" });
|
||
}
|
||
}
|
||
|
||
private void OnPlayerMove(PlayerMoveEvent e)
|
||
{
|
||
Velocity = e.Direction * Speed;
|
||
}
|
||
|
||
private void OnPlayerShoot(PlayerShootEvent e)
|
||
{
|
||
Shoot();
|
||
}
|
||
|
||
private void OnHealthChanged(int newHealth)
|
||
{
|
||
Logger.Debug($"Player health changed: {newHealth}");
|
||
|
||
// 受伤效果
|
||
if (newHealth < _playerModel.MaxHealth.Value)
|
||
{
|
||
FlashRed();
|
||
StartInvincibility();
|
||
}
|
||
}
|
||
|
||
private void OnAliveChanged(bool isAlive)
|
||
{
|
||
if (!isAlive)
|
||
{
|
||
Die();
|
||
}
|
||
}
|
||
|
||
private void Shoot()
|
||
{
|
||
Logger.Debug("Player shooting");
|
||
|
||
// 创建子弹
|
||
var bulletScene = GD.Load<PackedScene>("res://assets/scenes/Bullet.tscn");
|
||
var bullet = bulletScene.Instantiate<Bullet>();
|
||
|
||
// 设置子弹位置和方向
|
||
bullet.Position = Position;
|
||
bullet.Direction = Vector2.Up; // 向上射击
|
||
|
||
// 添加到场景
|
||
GetTree().Root.AddChild(bullet);
|
||
|
||
// 播放射击音效
|
||
PlayShootSound();
|
||
}
|
||
|
||
private void UpdateAnimation(Vector2 inputVector)
|
||
{
|
||
if (inputVector.Y < 0)
|
||
{
|
||
_animatedSprite.Play("up");
|
||
}
|
||
else if (inputVector.Y > 0)
|
||
{
|
||
_animatedSprite.Play("down");
|
||
}
|
||
else if (inputVector.X != 0)
|
||
{
|
||
_animatedSprite.Play("side");
|
||
_animatedSprite.FlipH = inputVector.X < 0;
|
||
}
|
||
else
|
||
{
|
||
_animatedSprite.Play("idle");
|
||
}
|
||
}
|
||
|
||
private void FlashRed()
|
||
{
|
||
_animatedSprite.Modulate = Colors.Red;
|
||
this.CreateSignalBuilder(_invincibilityTimer.SignalName.Timeout)
|
||
.Connect(() => _animatedSprite.Modulate = Colors.White)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
|
||
private void StartInvincibility()
|
||
{
|
||
SetCollisionLayerValue(1, false); // 关闭碰撞
|
||
_invincibilityTimer.Start();
|
||
}
|
||
|
||
private void HandleCollision(KinematicCollision2D collision)
|
||
{
|
||
var collider = collision.GetCollider();
|
||
|
||
if (collider is Enemy enemy && _playerModel.IsAlive.Value)
|
||
{
|
||
// 受到伤害
|
||
_playerModel.TakeDamage(enemy.Damage);
|
||
}
|
||
}
|
||
|
||
private void Die()
|
||
{
|
||
Logger.Info("Player died");
|
||
|
||
// 播放死亡动画
|
||
_animatedSprite.Play("death");
|
||
|
||
// 禁用碰撞
|
||
_collisionShape.Disabled = true;
|
||
|
||
// 发送玩家死亡事件
|
||
SendEvent(new PlayerDeathEvent());
|
||
|
||
// 延迟移除
|
||
this.CreateSignalBuilder(_animatedSprite.SignalName.AnimationFinished)
|
||
.WithFlags(ConnectFlags.OneShot)
|
||
.Connect(() => QueueFreeX())
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
|
||
private void PlayShootSound()
|
||
{
|
||
var audioPlayer = new AudioStreamPlayer();
|
||
var sound = GD.Load<AudioStream>("res://assets/audio/shoot.wav");
|
||
audioPlayer.Stream = sound;
|
||
AddChild(audioPlayer);
|
||
audioPlayer.Play();
|
||
|
||
// 音效播放完成后移除
|
||
this.CreateSignalBuilder(audioPlayer.SignalName.Finished)
|
||
.WithFlags(ConnectFlags.OneShot)
|
||
.Connect(() => audioPlayer.QueueFreeX())
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 创建 UI 系统
|
||
|
||
创建 `src/Game/UI.cs`:
|
||
|
||
```csharp
|
||
using Godot;
|
||
using GFramework.Godot.extensions;
|
||
using GFramework.Core.events;
|
||
using MyGFrameworkGame.Core.Models;
|
||
|
||
namespace MyGFrameworkGame
|
||
{
|
||
[ContextAware]
|
||
[Log]
|
||
public partial class UI : CanvasLayer
|
||
{
|
||
private Control _gameplayUI;
|
||
private Control _pauseMenu;
|
||
private Control _gameOverScreen;
|
||
private Label _scoreLabel;
|
||
private Label _healthLabel;
|
||
private Label _finalScoreLabel;
|
||
private Button _resumeButton;
|
||
private Button _quitButton;
|
||
private Button _restartButton;
|
||
|
||
private PlayerModel _playerModel;
|
||
private GameModel _gameModel;
|
||
|
||
public override void _Ready()
|
||
{
|
||
Logger.Info("UI ready");
|
||
|
||
// 获取 UI 组件引用
|
||
InitializeUIComponents();
|
||
|
||
// 设置上下文
|
||
_playerModel = Context.GetModel<PlayerModel>();
|
||
_gameModel = Context.GetModel<GameModel>();
|
||
|
||
// 注册事件监听
|
||
RegisterEventListeners();
|
||
|
||
// 注册按钮事件
|
||
RegisterButtonEvents();
|
||
|
||
// 显示主菜单
|
||
ShowMainMenu();
|
||
}
|
||
|
||
private void InitializeUIComponents()
|
||
{
|
||
// 游戏 UI
|
||
_gameplayUI = GetNode<Control>("GameplayUI");
|
||
_scoreLabel = GetNode<Label>("GameplayUI/ScoreLabel");
|
||
_healthLabel = GetNode<Label>("GameplayUI/HealthLabel");
|
||
|
||
// 暂停菜单
|
||
_pauseMenu = GetNode<Control>("PauseMenu");
|
||
_resumeButton = GetNode<Button>("PauseMenu/VBoxContainer/ResumeButton");
|
||
_quitButton = GetNode<Button>("PauseMenu/VBoxContainer/QuitButton");
|
||
|
||
// 游戏结束画面
|
||
_gameOverScreen = GetNode<Control>("GameOverScreen");
|
||
_finalScoreLabel = GetNode<Label>("GameOverScreen/CenterContainer/VBoxContainer/FinalScoreLabel");
|
||
_restartButton = GetNode<Button>("GameOverScreen/CenterContainer/VBoxContainer/RestartButton");
|
||
}
|
||
|
||
private void RegisterEventListeners()
|
||
{
|
||
// 监听分数变化
|
||
_playerModel.Score.Register(UpdateScoreDisplay)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
// 监听生命值变化
|
||
_playerModel.Health.Register(UpdateHealthDisplay)
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
|
||
private void RegisterButtonEvents()
|
||
{
|
||
// 暂停菜单按钮
|
||
this.CreateSignalBuilder(_resumeButton.SignalName.Pressed)
|
||
.Connect(() => {
|
||
_gameModel.ResumeGame();
|
||
GetTree().Paused = false;
|
||
ShowGameplayUI();
|
||
})
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
this.CreateSignalBuilder(_quitButton.SignalName.Pressed)
|
||
.Connect(() => {
|
||
GetTree().Quit();
|
||
})
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
|
||
// 重新开始按钮
|
||
this.CreateSignalBuilder(_restartButton.SignalName.Pressed)
|
||
.Connect(() => {
|
||
RestartGame();
|
||
})
|
||
.UnRegisterWhenNodeExitTree(this);
|
||
}
|
||
|
||
public void ShowMainMenu()
|
||
{
|
||
HideAllUI();
|
||
// TODO: 实现主菜单
|
||
Logger.Info("Showing main menu");
|
||
}
|
||
|
||
public void ShowGameplayUI()
|
||
{
|
||
HideAllUI();
|
||
_gameplayUI.Visible = true;
|
||
UpdateUI();
|
||
}
|
||
|
||
public void ShowPauseMenu()
|
||
{
|
||
_pauseMenu.Visible = true;
|
||
}
|
||
|
||
public void ShowGameOverScreen()
|
||
{
|
||
HideAllUI();
|
||
_gameOverScreen.Visible = true;
|
||
}
|
||
|
||
private void HideAllUI()
|
||
{
|
||
_gameplayUI.Visible = false;
|
||
_pauseMenu.Visible = false;
|
||
_gameOverScreen.Visible = false;
|
||
}
|
||
|
||
private void UpdateUI()
|
||
{
|
||
UpdateScoreDisplay(_playerModel.Score.Value);
|
||
UpdateHealthDisplay(_playerModel.Health.Value);
|
||
}
|
||
|
||
private void UpdateScoreDisplay(int score)
|
||
{
|
||
_scoreLabel.Text = $"Score: {score}";
|
||
}
|
||
|
||
private void UpdateHealthDisplay(int health)
|
||
{
|
||
_healthLabel.Text = $"Health: {health}";
|
||
|
||
// 根据生命值改变颜色
|
||
if (health <= 20)
|
||
{
|
||
_healthLabel.Modulate = Colors.Red;
|
||
}
|
||
else if (health <= 50)
|
||
{
|
||
_healthLabel.Modulate = Colors.Yellow;
|
||
}
|
||
else
|
||
{
|
||
_healthLabel.Modulate = Colors.White;
|
||
}
|
||
}
|
||
|
||
public void UpdateFinalScore(int score)
|
||
{
|
||
_finalScoreLabel.Text = $"Final Score: {score}";
|
||
}
|
||
|
||
private void RestartGame()
|
||
{
|
||
Logger.Info("Restarting game");
|
||
|
||
// 重新加载场景
|
||
GetTree().CallDeferred(SceneTree.MethodName.ReloadCurrentScene);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 测试验证
|
||
|
||
### 1. 创建测试项目
|
||
|
||
创建 `tests/Game.Tests/Game.Tests.csproj`:
|
||
|
||
```xml
|
||
<Project Sdk="Microsoft.NET.Sdk">
|
||
|
||
<PropertyGroup>
|
||
<TargetFramework>net6.0</TargetFramework>
|
||
<RootNamespace>MyGFrameworkGame.Tests</RootNamespace>
|
||
<Nullable>enable</Nullable>
|
||
<IsPackable>false</IsPackable>
|
||
</PropertyGroup>
|
||
|
||
<ItemGroup>
|
||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
|
||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||
<PackageReference Include="GeWuYou.GFramework.Core" Version="1.0.0" />
|
||
<PackageReference Include="GeWuYou.GFramework.Core.Tests" Version="1.0.0" />
|
||
</ItemGroup>
|
||
|
||
<ItemGroup>
|
||
<ProjectReference Include="..\..\src\Game.Core\Game.Core.csproj" />
|
||
</ItemGroup>
|
||
|
||
</Project>
|
||
```
|
||
|
||
### 2. 创建单元测试
|
||
|
||
创建 `tests/Game.Tests/PlayerModelTests.cs`:
|
||
|
||
```csharp
|
||
using NUnit.Framework;
|
||
using GFramework.Core.Tests;
|
||
using MyGFrameworkGame.Core.Models;
|
||
|
||
namespace MyGFrameworkGame.Tests
|
||
{
|
||
[TestFixture]
|
||
public class PlayerModelTests : ArchitectureTestsBase<TestArchitecture>
|
||
{
|
||
private PlayerModel _playerModel;
|
||
|
||
protected override TestArchitecture CreateArchitecture()
|
||
{
|
||
return new TestArchitecture();
|
||
}
|
||
|
||
[SetUp]
|
||
public void Setup()
|
||
{
|
||
Architecture.Initialize();
|
||
_playerModel = Architecture.Context.GetModel<PlayerModel>();
|
||
}
|
||
|
||
[Test]
|
||
public void PlayerModel_InitialValues_ShouldBeCorrect()
|
||
{
|
||
// Assert
|
||
Assert.That(_playerModel.Health.Value, Is.EqualTo(100));
|
||
Assert.That(_playerModel.MaxHealth.Value, Is.EqualTo(100));
|
||
Assert.That(_playerModel.Speed.Value, Is.EqualTo(300.0f));
|
||
Assert.That(_playerModel.Score.Value, Is.EqualTo(0));
|
||
Assert.That(_playerModel.IsAlive.Value, Is.True);
|
||
Assert.That(_playerModel.CanShoot.Value, Is.True);
|
||
}
|
||
|
||
[Test]
|
||
public void TakeDamage_ValidDamage_ShouldReduceHealth()
|
||
{
|
||
// Act
|
||
_playerModel.TakeDamage(20);
|
||
|
||
// Assert
|
||
Assert.That(_playerModel.Health.Value, Is.EqualTo(80));
|
||
Assert.That(_playerModel.IsAlive.Value, Is.True);
|
||
}
|
||
|
||
[Test]
|
||
public void TakeDamage_LethalDamage_ShouldKillPlayer()
|
||
{
|
||
// Act
|
||
_playerModel.TakeDamage(150);
|
||
|
||
// Assert
|
||
Assert.That(_playerModel.Health.Value, Is.EqualTo(0));
|
||
Assert.That(_playerModel.IsAlive.Value, Is.False);
|
||
}
|
||
|
||
[Test]
|
||
public void TakeDamage_WhenDead_ShouldNotChangeHealth()
|
||
{
|
||
// Arrange
|
||
_playerModel.TakeDamage(100); // Kill player
|
||
var healthAfterDeath = _playerModel.Health.Value;
|
||
|
||
// Act
|
||
_playerModel.TakeDamage(10);
|
||
|
||
// Assert
|
||
Assert.That(_playerModel.Health.Value, Is.EqualTo(healthAfterDeath));
|
||
}
|
||
|
||
[Test]
|
||
public void Heal_ValidAmount_ShouldRestoreHealth()
|
||
{
|
||
// Arrange
|
||
_playerModel.TakeDamage(30); // Health = 70
|
||
|
||
// Act
|
||
_playerModel.Heal(20);
|
||
|
||
// Assert
|
||
Assert.That(_playerModel.Health.Value, Is.EqualTo(90));
|
||
}
|
||
|
||
[Test]
|
||
public void Heal_ExcessAmount_ShouldNotExceedMaxHealth()
|
||
{
|
||
// Arrange
|
||
_playerModel.TakeDamage(50); // Health = 50
|
||
|
||
// Act
|
||
_playerModel.Heal(100); // Try to heal more than max
|
||
|
||
// Assert
|
||
Assert.That(_playerModel.Health.Value, Is.EqualTo(100)); // Should cap at max
|
||
}
|
||
|
||
[Test]
|
||
public void CanShoot_WhenAliveAndNotOnCooldown_ShouldReturnTrue()
|
||
{
|
||
// Act & Assert
|
||
Assert.That(_playerModel.CanShoot(), Is.True);
|
||
}
|
||
|
||
[Test]
|
||
public void CanShoot_WhenDead_ShouldReturnFalse()
|
||
{
|
||
// Arrange
|
||
_playerModel.TakeDamage(100);
|
||
|
||
// Act & Assert
|
||
Assert.That(_playerModel.CanShoot(), Is.False);
|
||
}
|
||
|
||
[Test]
|
||
public void CanShoot_WhenOnCooldown_ShouldReturnFalse()
|
||
{
|
||
// Arrange
|
||
_playerModel.CanShoot.Value = false;
|
||
|
||
// Act & Assert
|
||
Assert.That(_playerModel.CanShoot(), Is.False);
|
||
}
|
||
}
|
||
|
||
// 测试架构
|
||
public class TestArchitecture : Architecture
|
||
{
|
||
protected override void Init()
|
||
{
|
||
RegisterModel(new PlayerModel());
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 运行测试
|
||
|
||
```bash
|
||
cd tests/Game.Tests
|
||
dotnet test
|
||
```
|
||
|
||
### 4. 集成测试
|
||
|
||
创建 `tests/Game.Tests/GameIntegrationTests.cs`:
|
||
|
||
```csharp
|
||
using NUnit.Framework;
|
||
using Godot;
|
||
using MyGFrameworkGame;
|
||
|
||
namespace MyGFrameworkGame.Tests
|
||
{
|
||
[TestFixture]
|
||
public class GameIntegrationTests
|
||
{
|
||
private SceneTree _sceneTree;
|
||
private MainScene _mainScene;
|
||
|
||
[SetUp]
|
||
public void Setup()
|
||
{
|
||
// 创建测试场景树
|
||
_sceneTree = new SceneTree();
|
||
_sceneTree.Root = new Node();
|
||
|
||
// 创建主场景
|
||
_mainScene = new MainScene();
|
||
_sceneTree.Root.AddChild(_mainScene);
|
||
}
|
||
|
||
[TearDown]
|
||
public void TearDown()
|
||
{
|
||
_mainScene?.QueueFree();
|
||
_sceneTree?.Quit();
|
||
}
|
||
|
||
[Test]
|
||
public void MainScene_Ready_ShouldInitializeCorrectly()
|
||
{
|
||
// Act
|
||
_mainScene._Ready();
|
||
|
||
// Wait a frame for initialization
|
||
_sceneTree.ProcessFrame();
|
||
|
||
// Assert
|
||
Assert.That(_mainScene.GetChildCount(), Is.GreaterThan(0));
|
||
// 验证架构已初始化
|
||
var context = _mainScene.Context;
|
||
Assert.That(context, Is.Not.Null);
|
||
}
|
||
|
||
[Test]
|
||
public void Game_StartGame_ShouldChangeGameStateToPlaying()
|
||
{
|
||
// Arrange
|
||
_mainScene._Ready();
|
||
_sceneTree.ProcessFrame();
|
||
|
||
var gameModel = _mainScene.Context.GetModel<GameModel>();
|
||
|
||
// Act
|
||
gameModel.StartGame();
|
||
|
||
// Assert
|
||
Assert.That(gameModel.State.Value, Is.EqualTo(GameState.Playing));
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 项目打包
|
||
|
||
### 1. 配置导出设置
|
||
|
||
1. 在 Godot 编辑器中,点击 "项目" → "导出"
|
||
2. 添加 Windows/macOS/Linux 导出预设
|
||
3. 配置导出选项:
|
||
- **目标**: 选择目标平台
|
||
- **调试信息**: 发布版本选择 "无"
|
||
- **优化级别**: "速度优化"
|
||
- **PCK 加密**: 可选的安全选项
|
||
|
||
### 2. 创建导出脚本
|
||
|
||
创建 `scripts/export.py`:
|
||
|
||
```python
|
||
# Godot 导出脚本
|
||
import os
|
||
|
||
def export_game():
|
||
# Windows 导出
|
||
os.system("godot --headless --export-release \"Windows Desktop\" build/windows/game.exe")
|
||
|
||
# macOS 导出
|
||
os.system("godot --headless --export-release \"macOS\" build/macOS/game.zip")
|
||
|
||
# Linux 导出
|
||
os.system("godot --headless --export-release \"Linux/X11\" build/linux/game.x86_64")
|
||
|
||
if __name__ == "__main__":
|
||
export_game()
|
||
```
|
||
|
||
### 3. 自动化构建
|
||
|
||
创建 `.github/workflows/build.yml`:
|
||
|
||
```yaml
|
||
name: Build Game
|
||
|
||
on:
|
||
push:
|
||
branches: [ main ]
|
||
pull_request:
|
||
branches: [ main ]
|
||
|
||
jobs:
|
||
build:
|
||
runs-on: ${{ matrix.os }}
|
||
strategy:
|
||
matrix:
|
||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||
|
||
steps:
|
||
- uses: actions/checkout@v3
|
||
|
||
- name: Setup .NET
|
||
uses: actions/setup-dotnet@v3
|
||
with:
|
||
dotnet-version: '6.0.x'
|
||
|
||
- name: Restore dependencies
|
||
run: dotnet restore src/Game/Game.csproj
|
||
|
||
- name: Build
|
||
run: dotnet build src/Game/Game.csproj --no-restore
|
||
|
||
- name: Test
|
||
run: dotnet test tests/Game.Tests/Game.Tests.csproj --no-build --verbosity normal
|
||
|
||
- name: Setup Godot
|
||
uses: chickensoft-games/setup-godot@v1
|
||
with:
|
||
version: 4.5.1
|
||
|
||
- name: Export Game
|
||
run: |
|
||
mkdir -p build
|
||
godot --headless --export-release "Windows Desktop" build/game.exe
|
||
```
|
||
|
||
## 进阶功能
|
||
|
||
### 1. 添加音效系统
|
||
|
||
创建 `src/Game.Core/Systems/AudioSystem.cs`:
|
||
|
||
```csharp
|
||
using Godot;
|
||
using GFramework.Core.system;
|
||
using MyGFrameworkGame.Core.Models;
|
||
|
||
namespace MyGFrameworkGame.Core.Systems
|
||
{
|
||
public class AudioSystem : AbstractSystem
|
||
{
|
||
private AudioStreamPlayer _musicPlayer;
|
||
private AudioStreamPlayer _sfxPlayer;
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// 注册音频事件监听
|
||
RegisterEvent<PlayerShootEvent>(OnPlayerShoot);
|
||
RegisterEvent<PlayerDeathEvent>(OnPlayerDeath);
|
||
RegisterEvent<GameStartEvent>(OnGameStart);
|
||
}
|
||
|
||
private void OnPlayerShoot(PlayerShootEvent e)
|
||
{
|
||
PlaySound("shoot");
|
||
}
|
||
|
||
private void OnPlayerDeath(PlayerDeathEvent e)
|
||
{
|
||
PlaySound("death");
|
||
}
|
||
|
||
private void OnGameStart(GameStartEvent e)
|
||
{
|
||
PlayMusic("background");
|
||
}
|
||
|
||
private void PlaySound(string soundName)
|
||
{
|
||
var sound = GD.Load<AudioStream>($"res://assets/audio/{soundName}.wav");
|
||
_sfxPlayer.Stream = sound;
|
||
_sfxPlayer.Play();
|
||
}
|
||
|
||
private void PlayMusic(string musicName)
|
||
{
|
||
var music = GD.Load<AudioStream>($"res://assets/audio/{musicName}.ogg");
|
||
_musicPlayer.Stream = music;
|
||
_musicPlayer.Play();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 添加存档系统
|
||
|
||
创建 `src/Game.Core/Systems/SaveSystem.cs`:
|
||
|
||
```csharp
|
||
using System.IO;
|
||
using GFramework.Core.system;
|
||
using GFramework.Core.command;
|
||
using MyGFrameworkGame.Core.Models;
|
||
using Newtonsoft.Json;
|
||
|
||
namespace MyGFrameworkGame.Core.Systems
|
||
{
|
||
public class SaveSystem : AbstractSystem
|
||
{
|
||
private const string SavePath = "user://saves/";
|
||
|
||
protected override void OnInit()
|
||
{
|
||
// 创建保存目录
|
||
if (!DirAccess.DirExistsAbsolute(SavePath))
|
||
{
|
||
DirAccess.Open("user://").MakeDir("saves");
|
||
}
|
||
|
||
// 注册保存命令
|
||
RegisterCommand<SaveGameCommand>(SaveGame);
|
||
RegisterCommand<LoadGameCommand>(LoadGame);
|
||
}
|
||
|
||
private void SaveGame(SaveGameCommand command)
|
||
{
|
||
var playerModel = GetModel<PlayerModel>();
|
||
var gameModel = GetModel<GameModel>();
|
||
|
||
var saveData = new SaveData
|
||
{
|
||
PlayerHealth = playerModel.Health.Value,
|
||
PlayerScore = playerModel.Score.Value,
|
||
CurrentLevel = gameModel.CurrentLevel.Value,
|
||
Timestamp = System.DateTime.Now
|
||
};
|
||
|
||
var json = JsonConvert.SerializeObject(saveData, Formatting.Indented);
|
||
var file = FileAccess.Open($"{SavePath}save_{command.SlotId}.json", FileAccess.ModeFlags.Write);
|
||
file.StoreString(json);
|
||
file.Close();
|
||
}
|
||
|
||
private void LoadGame(LoadGameCommand command)
|
||
{
|
||
var filePath = $"{SavePath}save_{command.SlotId}.json";
|
||
|
||
if (!FileAccess.FileExists(filePath))
|
||
{
|
||
GD.PrintErr($"Save file not found: {filePath}");
|
||
return;
|
||
}
|
||
|
||
var file = FileAccess.Open(filePath, FileAccess.ModeFlags.Read);
|
||
var json = file.GetAsText();
|
||
file.Close();
|
||
|
||
var saveData = JsonConvert.DeserializeObject<SaveData>(json);
|
||
|
||
// 恢复游戏状态
|
||
var playerModel = GetModel<PlayerModel>();
|
||
var gameModel = GetModel<GameModel>();
|
||
|
||
playerModel.Health.Value = saveData.PlayerHealth;
|
||
playerModel.Score.Value = saveData.PlayerScore;
|
||
gameModel.CurrentLevel.Value = saveData.CurrentLevel;
|
||
}
|
||
}
|
||
|
||
public class SaveData
|
||
{
|
||
public int PlayerHealth { get; set; }
|
||
public int PlayerScore { get; set; }
|
||
public int CurrentLevel { get; set; }
|
||
public System.DateTime Timestamp { get; set; }
|
||
}
|
||
|
||
public class SaveGameCommand : AbstractCommand
|
||
{
|
||
public int SlotId { get; set; }
|
||
}
|
||
|
||
public class LoadGameCommand : AbstractCommand
|
||
{
|
||
public int SlotId { get; set; }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 添加本地化支持
|
||
|
||
创建 `src/Game.Core/Systems/LocalizationSystem.cs`:
|
||
|
||
```csharp
|
||
using System.Collections.Generic;
|
||
using Godot;
|
||
using GFramework.Core.system;
|
||
|
||
namespace MyGFrameworkGame.Core.Systems
|
||
{
|
||
public class LocalizationSystem : AbstractSystem
|
||
{
|
||
private Dictionary<string, string> _translations = new();
|
||
private string _currentLanguage = "en";
|
||
|
||
protected override void OnInit()
|
||
{
|
||
LoadLanguage(_currentLanguage);
|
||
}
|
||
|
||
public void SetLanguage(string languageCode)
|
||
{
|
||
_currentLanguage = languageCode;
|
||
LoadLanguage(languageCode);
|
||
}
|
||
|
||
public string GetText(string key)
|
||
{
|
||
return _translations.GetValueOrDefault(key, key);
|
||
}
|
||
|
||
private void LoadLanguage(string languageCode)
|
||
{
|
||
var translationFile = GD.Load<Translation>($"res://assets/locales/{languageCode}.po");
|
||
if (translationFile != null)
|
||
{
|
||
TranslationServer.SetLocale(languageCode);
|
||
GD.Print($"Loaded language: {languageCode}");
|
||
}
|
||
else
|
||
{
|
||
GD.PrintErr($"Failed to load language: {languageCode}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
恭喜!你已经成功创建了一个完整的 GFramework 游戏项目。这个项目包含了:
|
||
|
||
- ✅ **完整的架构设计** - 使用 GFramework 的五层架构
|
||
- ✅ **丰富的游戏功能** - 玩家控制、UI、音效、存档等
|
||
- ✅ **完善的测试** - 单元测试和集成测试
|
||
- ✅ **自动化构建** - CI/CD 流水线配置
|
||
|
||
### 下一步建议
|
||
|
||
1. **扩展游戏内容**:添加更多敌人、关卡、道具
|
||
2. **优化性能**:使用对象池、减少内存分配
|
||
3. **添加网络功能**:多人游戏、排行榜
|
||
4. **完善 UI**:更丰富的界面和动画
|
||
5. **发布游戏**:在各个平台发布你的作品
|
||
|
||
### 学习资源
|
||
|
||
- [GFramework 主文档](../../README.md)
|
||
- [GFramework.Core 文档](../../GFramework.Core/README.md)
|
||
- [GFramework.Godot 文档](../../GFramework.Godot/README.md)
|
||
- [GFramework.SourceGenerators 文档](../../GFramework.SourceGenerators/README.md)
|
||
|
||
享受游戏开发的乐趣吧!🎮
|
||
|
||
---
|
||
|
||
**教程版本**: 1.0.0
|
||
**更新日期**: 2026-01-12 |