4. 你的第一个脚本

解锁整个框架的那一个模式,外加你最先会用到的五个服务的可复制粘贴代码片段。

4.1 快速开始

新建一个 C# 脚本,把下面的代码粘进去,挂到任意场景中的任意 GameObject 上,然后按下 Play:

using CommonGameSystem.Core;
using UnityEngine;

public class HelloCgs : MonoBehaviour
{
    private void Start()
    {
        var scheduler = ServiceLocator.Resolve<IScheduler>();
        var tween     = ServiceLocator.Resolve<ITweenService>();

        scheduler.After(1f, () => Debug.Log("One second after Play."));

        tween.To(1f, 2f, 0.5f,
            scale => transform.localScale = Vector3.one * scale,
            EaseType.OutBack);
    }
}

对象带着弹性过冲弹到两倍大小,一秒后一条消息出现。注意缺了什么:没有管理器预制体,没有初始化场景,没有设置组件。引导程序在你的 Start 运行之前就注册好了全部 23 个服务。

4.2 服务解析是如何工作的

每个 CGS 服务的获取方式都相同:向服务定位器索取你需要的接口。

var save = ServiceLocator.Resolve<ISaveService>();

三条规则让这既快又安全:

  • 尽早解析,缓存结果。 服务在第一个场景加载之前就已注册,所以在 AwakeStart 中解析都是安全的。把引用存进字段并复用——绝不要在 Update 里调用 Resolve
  • 缺失的服务会抛异常。 如果某个接口没有注册任何实现,Resolve<T>() 会抛出一个含义清晰的异常。想做软检查,用 ServiceLocator.TryResolve(out T service)ServiceLocator.IsRegistered<T>()
  • 一切都可以替换。 ServiceLocator.Replace<T>(instance) 可以在任何时刻换上你自己的实现,或一个什么都不做的空版本(第 1 章 1.4 节)。

典型的服务使用方长这样:

public class GameHud : MonoBehaviour
{
    private ISaveService _save;   // cached once, used many times

    private void Awake()
    {
        _save = ServiceLocator.Resolve<ISaveService>();
    }
}

4.3 你最先会用到的五个服务

下面每段代码都运行在一个 MonoBehaviour 内部,并沿用 4.1 节的 using 语句。

存档与读档

任何可序列化的类都可以成为存档文件。写入是原子性的——存档进行到一半时崩溃,也绝不会损坏之前的文件。完整参考:Save/Load

[System.Serializable]
public class PlayerData { public int Level; public float Health; }

var save = ServiceLocator.Resolve<ISaveService>();
save.Save("slot0", new PlayerData { Level = 3, Health = 75f });

var result = save.Load<PlayerData>("slot0");
if (result.Status == SaveStatus.Ok)
    Debug.Log("Loaded level " + result.Value.Level);

事件

任何类都可以成为事件。发布者和订阅者彼此从不引用——它们只共享事件类型。完整参考:Event Bus

public sealed class CoinCollected { public int Amount; }

var events = ServiceLocator.Resolve<IEventBus>();
System.IDisposable ticket = events.Subscribe<CoinCollected>(
    e => Debug.Log("Coins gained: " + e.Amount));

events.Publish(new CoinCollected { Amount = 5 });
ticket.Dispose();   // stop listening (do this in OnDestroy)

定时器

定时器按被调度的先后顺序触发。一次性定时器会自行清理;循环定时器会一直运行,直到你将其释放。完整参考:Scheduler

var scheduler = ServiceLocator.Resolve<IScheduler>();

scheduler.After(2f, () => Debug.Log("Two seconds later, exactly once."));

System.IDisposable heartbeat =
    scheduler.Every(0.5f, () => Debug.Log("Every half second."));
heartbeat.Dispose();   // stop the repeating timer when done

补间

补间把一个值从一个数字动画到另一个数字,并把每一步交给你处理。补间默认运行在游戏玩法时钟上,因此游戏暂停时它们也随之暂停。完整参考:Tween

var tween = ServiceLocator.Resolve<ITweenService>();
CanvasGroup group = GetComponent<CanvasGroup>();

tween.To(1f, 0f, 0.75f,
    alpha => group.alpha = alpha,
    EaseType.OutQuad,
    onComplete: () => Debug.Log("Fade finished."));

输入

输入通过你的输入动作资产中的命名动作映射来读取。把 "Gameplay""Jump" 换成你自己资产里的名字。为 InputAction 类型添加 using UnityEngine.InputSystem;。完整参考:Input

private IInputService _input;
private InputAction _jump;

private void Start()
{
    _input = ServiceLocator.Resolve<IInputService>();
    _jump  = _input.GetAction("Gameplay", "Jump");
}

private void Update()
{
    if (_jump != null && _input.WasPressedThisFrame(_jump))
        Debug.Log("Jump!");
}

4.4 接下来去哪

你现在已经掌握了解锁整个框架的那一个模式:解析接口、缓存、调用。接下来:

  • 服务参考。 每个服务都有自己的页面——从文档索引开始,或在你的项目里查看 Assets/CommonGameSystem.Core/Documentation/Modules/(例如 SaveLoad.mdTween.md)。每页都覆盖完整 API、常见模式和真正要紧的注意事项。
  • 示例。 RPG Starter Sample(第 3 章)展示了上面几个服务在一个小型游戏切片中协作的样子。
  • 本手册的其余部分。 第 5 章讲解五个核心概念;第 6 章把框架连接到你自己的资产。
  • 更新日志。 Welcome 窗口的快捷链接(第 3 章)可以打开你所安装版本的文档和更新日志。

下一章:5. 核心概念