1. 项目概述为什么选择Photon Fusion 2如果你正在开发一款Unity多人游戏并且对网络同步的复杂性感到头疼——比如如何处理玩家位置、物理状态、输入预测和滞后补偿——那么Photon Fusion 2很可能就是你正在寻找的解决方案。它不是简单的网络消息库而是一个完整的网络状态同步框架。我最初接触它是因为一个需要高精度、低延迟同步的竞技类项目传统的UNet或者Photon PUN在应对快速移动物体和复杂状态同步时总感觉有些力不从心要么是“橡皮筋”效应严重要么是客户端预测实现起来异常繁琐。Fusion 2的核心思想是“状态同步”而非“指令同步”它帮你管理了游戏世界中几乎所有对象的网络化状态让你能像编写单机游戏一样编写网络逻辑这极大地降低了开发门槛。简单来说引入Fusion 2意味着你将获得一套开箱即用的、带客户端预测和服务器权威验证的网络架构。这对于制作射击游戏、赛车游戏、实时策略游戏或者任何对同步要求苛刻的多人体验来说是至关重要的基础。本篇文章我将以一个从零开始的Unity项目为例手把手带你完成Photon Fusion 2的初始化引入过程。这个过程不仅仅是点击几个按钮我会详细解释每一步背后的意图以及你可能遇到的坑和对应的解决方案确保你能顺利搭建起开发环境而不是卡在莫名其妙的错误上。2. 环境准备与账号配置在开始敲代码之前我们需要把“地基”打好。这个阶段的工作看似琐碎但至关重要一步错可能导致后续所有步骤都无法进行。很多新手容易在这里栽跟头比如SDK版本不匹配、账号没配置好导致无法连接服务器等。2.1 Unity版本与项目设置首先确保你的Unity版本与Photon Fusion 2兼容。根据官方文档和我的实践经验Unity 2021.3 LTS或2022.3 LTS是目前最稳定、支持最完善的选择。我强烈建议使用LTS长期支持版本以避免遇到一些前沿版本可能存在的未知兼容性问题。你可以通过Unity Hub轻松安装或切换版本。创建一个新的Unity项目时我建议使用3D核心模板。虽然Fusion也支持URP通用渲染管线和HDRP高清渲染管线但在初始化阶段使用内置渲染管线或3D核心模板能最大程度减少因渲染管线导致的额外配置问题。项目创建好后第一件事是去Edit - Project Settings - Player中确认一下Api Compatibility Level设置为.NET Standard 2.1或.NET Framework如果目标是PC平台。Fusion的一些高级特性依赖于较新的.NET API使用旧的.NET Standard 2.0可能会导致编译错误。注意如果你计划发布到WebGL平台需要提前知晓Fusion对WebGL的支持有其特殊性主要是网络协议方面初始化步骤大体相同但后续配置有差异。本文主要基于PC/移动平台的标准流程。2.2 Photon账号与App ID获取Photon Fusion的服务运行在Photon Cloud上因此你需要一个Photon账号来创建和管理你的“应用程序”。这相当于你的游戏服务器在云端的唯一标识。注册与登录访问Photon Engine官网注册一个账号。他们的免费套餐Photon Fusion Free对于小规模测试和原型开发是完全足够的支持最多20个并发用户这非常适合项目初期。创建Fusion App登录后进入Dashboard控制面板。点击“Create a New App”。这里的关键是选择“Fusion”作为类型而不是“PUN”或“Quantum”。如果选错后续将无法使用Fusion的SDK进行连接。获取关键凭证创建成功后进入该App的设置页面。你需要找到两个核心信息App Id Fusion: 这是Fusion服务专用的应用程序ID格式通常像一串较长的哈希值。Region区域选择离你的目标玩家群体最近的服务器区域例如asia亚洲、eu欧洲或us美国。这直接影响网络延迟。请妥善保管你的App Id Fusion我们很快就会在Unity项目中用到它。一个常见的错误是复制了“App Id PUN”或者“App Id Chat”这会导致初始化失败提示认证错误。3. 导入Fusion SDK与基础配置有了账号和项目基础现在可以把Fusion SDK“请”进你的Unity工程了。这一步的细节决定了SDK是否能正确集成并工作。3.1 通过Unity Package Manager导入这是官方推荐的、最清晰的管理方式。打开Unity进入Window - Package Manager。在Package Manager窗口的左上角点击“”号选择“Add package from git URL...”。你需要输入Fusion SDK的Git仓库地址。请注意Fusion的开发相对活跃直接使用主分支有时可能遇到不稳定情况。为了稳定起见我建议使用一个特定的版本标签。例如你可以输入https://github.com/photonengine/photon-fusion-sdk.git#v2.0.0请前往Photon Fusion的GitHub发布页面查看最新的稳定版本号替换v2.0.0。点击“Add”后Unity会开始下载并导入整个Fusion SDK包。这个过程可能会花费几分钟取决于你的网速。导入完成后你会在Package Manager的列表里看到“Photon Fusion”包以及其相关的依赖项如“Photon Libs”。实操心得有时从Git URL导入会因网络问题失败。备选方案是直接从GitHub Releases页面下载对应的.unitypackage文件然后通过Assets - Import Package - Custom Package...进行传统导入。但Package Manager方式更便于后续更新和依赖管理是首选。3.2 运行安装向导SDK导入成功后Unity编辑器顶部菜单栏会出现一个新的“Fusion”菜单。点击它选择“Fusion - Install Fusion”。这个安装向导会引导你完成一系列自动化配置是初始化流程的核心。它会检查环境验证Unity版本、.NET配置等。导入必备资源自动导入Network Project Config、基础Prefab、输入系统预制件等核心资源到你的项目Assets文件夹中通常是在Assets/Photon/Fusion/目录下。配置输入系统Fusion使用一种名为“Fusion Input”的抽象层来处理网络化输入。向导会帮你初始化默认的输入配置。请耐心等待向导完成所有步骤直到出现“Installation Complete”之类的提示。如果中途遇到错误例如文件写入权限问题请根据控制台Console中的红色错误信息进行排查。最常见的问题是项目路径包含中文或特殊字符请确保你的Unity项目存放在一个纯英文路径下。3.3 配置Network Project Config安装向导完成后你会在Assets/Photon/Fusion/目录下找到一个名为NetworkProjectConfig.asset的文件。这是Fusion项目的核心配置文件相当于你整个网络游戏世界的“宪法”。双击打开它在Inspector面板中你需要填写最关键的一步在“Photon App Id Fusion”字段中粘贴你之前在Photon Dashboard中获取的App Id Fusion。确认“Default Region”设置为你选择的区域。这个配置文件还管理着很多其他重要设置比如Prefab Table: 用于网络动态实例化对象的预制体注册表。未来你所有需要通过网络生成的GameObject如玩家角色、子弹、道具都需要在这里注册。Networked Scene Management: 网络场景加载的配置。SimulationTick率每秒网络更新次数等底层模拟设置。对于大多数实时动作游戏Tick Rate设置为30是一个不错的起点它平衡了流畅度和网络带宽。至此Fusion SDK已经在你的项目中安家落户并且知道了该连接到哪里。但我们的游戏世界还没有任何网络化的对象。4. 创建第一个网络化场景与对象配置好SDK后我们要创建一个最简单的可运行的网络示例来验证一切是否正常。这个过程会让你理解Fusion运作的基本单元。4.1 设置Network RunnerNetworkRunner是Fusion的“大脑”和“心脏”它是管理所有网络连接、状态同步和模拟循环的单例组件。每个需要参与网络会话的游戏实例无论是主机还是客户端都必须有一个NetworkRunner。在场景中创建一个空的GameObject命名为“Network Runner”。选中它在Inspector面板中点击“Add Component”搜索并添加“Network Runner”组件。保持其默认设置即可。一个重要的属性是Runner Prefab如果你希望在其他场景也使用同一个Runner可以把它做成预制体并拖拽到这里实现跨场景持久化。对于第一个简单场景我们直接使用场景中的对象就行。4.2 创建基础的Network ObjectNetworkObject是Fusion中所有需要同步的物体的基础组件。它赋予了一个GameObject在网络世界中唯一的身份标识Network Id。在场景中创建一个Cube或其他任何3D物体命名为“Player”。选中“Player”点击“Add Component”搜索并添加“Network Object”组件。添加Network Object后你可能会注意到它自动要求你添加一个Network Transform组件。先点击“Add”同意添加。Network Transform是用于同步物体位置、旋转和缩放的核心组件对于玩家角色这样的移动物体是必需的。现在你的“Player” GameObject上应该有三个关键组件TransformUnity原生、Network Object和Network Transform。4.3 编写简易的玩家生成与连接脚本我们需要一个脚本来启动NetworkRunner并让玩家加入游戏。创建一个新的C#脚本命名为BasicNetworkLauncher。using Fusion; using UnityEngine; public class BasicNetworkLauncher : MonoBehaviour { // 在Inspector中指定我们的Player预制体 [SerializeField] private NetworkObject _playerPrefab; private NetworkRunner _runner; async void Start() { // 1. 创建并初始化NetworkRunner _runner gameObject.AddComponentNetworkRunner(); _runner.ProvideInput true; // 声明此Runner将提供玩家输入 // 2. 启动Runner。StartGame是异步方法需要用await var startGameArgs new StartGameArgs() { GameMode GameMode.Shared, // 使用Shared模式类似P2P主机模式 SessionName MyFirstFusionRoom, // 房间名 PlayerCount 4, // 最大玩家数 Scene 1, // 启动后要加载的场景索引当前场景为0可以指定其他场景 SceneManager gameObject.AddComponentNetworkSceneManagerDefault() // 场景管理器 }; await _runner.StartGame(startGameArgs); // 3. Runner启动成功后为本地玩家生成一个网络实体 if (_runner.IsServer) // 在Shared模式下第一个启动的玩家就是Server { // 在随机位置生成玩家预制体 Vector3 spawnPosition new Vector3(Random.Range(-3, 3), 1, Random.Range(-3, 3)); _runner.Spawn(_playerPrefab, spawnPosition, Quaternion.identity, _runner.LocalPlayer); } } }将这个脚本挂载到“Network Runner”游戏对象上。然后将场景中的“Player”对象拖拽成预制体拖入Assets文件夹再将这个预制体从Assets拖拽到BasicNetworkLauncher脚本的_playerPrefab字段上。最后将场景中的“Player”实例删除。因为玩家角色将由网络动态生成而不是静态放在场景里。4.4 配置输入与基础移动为了让我们的玩家动起来我们需要处理输入。Fusion推荐使用其自带的输入系统。找到Assets/Photon/Fusion/Resources/FusionInputSettings这个资源文件。你可以在这里定义虚拟的“按钮”和“轴向”。为了简单测试我们直接使用默认的“Horizontal”和“Vertical”轴向它们默认映射到键盘AD和WS。修改“Player”预制体为其添加一个新的脚本BasicPlayerMovementusing Fusion; using UnityEngine; public class BasicPlayerMovement : NetworkBehaviour { // 这是一个Networked属性它的值会在所有客户端间自动同步 [Networked] private Angle Yaw { get; set; } [Networked] private Angle Pitch { get; set; } private NetworkCharacterController _ncc; public override void Spawned() { // 获取网络角色控制器它提供了网络感知的移动方法 _ncc GetComponentNetworkCharacterController(); if (_ncc null) { _ncc gameObject.AddComponentNetworkCharacterController(); } } public override void FixedUpdateNetwork() { // FixedUpdateNetwork是Fusion的固定网络帧更新循环 // 只有在这里才能安全地读取输入和修改Networked属性 if (GetInput(out NetworkInputData input)) { // 从输入数据中读取移动向量 Vector3 moveDirection new Vector3(input.Direction.x, 0, input.Direction.y); // 使用NetworkCharacterController进行移动它会处理客户端预测和校正 _ncc.Move(moveDirection * 5f * Runner.DeltaTime); } } } // 定义一个网络输入数据结构 public struct NetworkInputData : INetworkInput { public Vector2 Direction; // 存储移动方向 }我们还需要一个脚本来收集本地输入并将其传递给Fusion。创建一个BasicInput脚本挂载到任何持久化的对象上比如挂载Network Runner的那个对象using Fusion; using UnityEngine; public class BasicInput : MonoBehaviour { private NetworkRunner _runner; void Update() { if (_runner null) _runner FindObjectOfTypeNetworkRunner(); // 简单查找生产环境建议用更好的方式获取 if (_runner ! null _runner.GetLocalPlayerInputPrototypeNetworkInputData() is NetworkInputData input) { // 收集原始输入 input.Direction.x Input.GetAxisRaw(Horizontal); input.Direction.y Input.GetAxisRaw(Vertical); // 将输入设置给Runner _runner.SetLocalPlayerInput(input); } } }现在基本的网络化移动就设置好了。NetworkCharacterController组件会自动处理地面的碰撞检测。5. 构建、运行与基础测试理论配置完成是时候看到实际效果了。这是验证你所有配置是否正确的最直接方法。5.1 构建独立应用进行本地测试在Unity编辑器中打开File - Build Settings。将当前场景添加到“Scenes In Build”列表中。选择目标平台例如PC, Mac Linux Standalone点击“Build”并选择一个输出文件夹。等待构建完成。运行构建出来的可执行文件.exe等。这个进程将作为主机Host。不要关闭这个主机进程。回到Unity编辑器直接点击Play按钮运行游戏。编辑器中的实例将作为客户端Client连接到主机。此时你应该能看到两个窗口一个是你构建的独立应用一个是Unity编辑器。每个窗口中都会有一个由网络生成的Cube玩家。当你在一个窗口中按下WASD键移动Cube时另一个窗口中的Cube应该也会相应地平滑移动可能会有极小的预测和校正但基本是即时的。恭喜这标志着你的Photon Fusion 2基础环境已经成功搭建并运行起来了。你创建了一个网络会话两个独立的游戏实例通过网络同步了一个游戏对象的状态。5.2 常见连接问题排查如果连接失败或者看不到对方请按以下步骤检查检查控制台日志无论是在独立应用还是编辑器中查看Console窗口。Fusion和Photon会输出非常详细的日志错误信息通常一目了然。“AppId not found”或“Authentication failed” 百分之百是NetworkProjectConfig.asset中的Photon App Id Fusion填错了或者你创建的不是Fusion类型的App。请仔细核对。“Failed to connect to region xxx” 网络问题。检查防火墙是否阻止了UDP端口默认是5055和5056。也可以尝试在NetworkProjectConfig中切换其他Region测试。“No scene loaded”或场景不同步 检查BasicNetworkLauncher中StartGameArgs的Scene参数是否正确以及NetworkSceneManagerDefault组件是否已添加。检查预制体注册 我们的“Player”预制体目前是通过脚本直接Spawn的所以暂时不需要在NetworkProjectConfig的Prefab Table中注册。但如果你遇到Spawn调用失败提示预制体未找到就需要去那里注册。对于更复杂的项目注册是必须的。检查Network Object权限 在Network Object组件上有一个Authority权限的概念。默认情况下生成该物体的玩家或服务器拥有其状态权威。在我们的简单脚本中主机为所有人生成玩家所以权限是清晰的。如果后续你遇到物体不受控制的问题需要检查权限设置。6. 深入理解从初始化到运行的内部流程完成了基础搭建我们有必要深入一层理解一下当你点击播放时Fusion在背后到底做了什么。这能帮助你在遇到更复杂问题时知道从哪里入手分析。6.1 NetworkRunner的生命周期NetworkRunner的StartGame()方法是整个网络会话的起点。它内部经历了初始化 根据GameModeShared,Host,Client,Server初始化本地角色。Shared模式是最简单的P2P模式第一个启动的玩家自动成为“服务器”拥有状态仲裁权。连接Photon Cloud 使用你配置的App Id和Region与Photon的云服务器建立WebSocket或UDP连接。加入或创建房间 根据SessionName要么加入一个已存在的房间要么创建一个新的。加载场景 通过NetworkSceneManagerDefault加载指定的场景并通知所有客户端同步加载。开始模拟循环 启动一个固定的FixedUpdateNetwork循环。这个循环的频率由NetworkProjectConfig中的Tick Rate决定如30Hz。所有网络状态的更新和物理计算都应该放在这个循环的回调中而不是Unity默认的Update或FixedUpdate这样才能保证所有客户端模拟的一致性。6.2 状态同步与预测回滚的核心Fusion被称为“状态同步”框架但它的精髓在于“带预测和回滚的状态同步”。Networked属性 当你用[Networked]标记一个属性如我们脚本中的Yaw和PitchFusion就会自动为这个属性生成网络同步代码。它的值变化会被自动捕获并在网络帧Tick间同步。输入收集与预测 在FixedUpdateNetwork中通过GetInput(out T data)可以获取到本地玩家的输入。NetworkCharacterController.Move()这样的方法在客户端执行时会立即根据输入移动角色预测同时将输入发送给服务器。服务器仲裁与校正 服务器收到所有客户端的输入后在相同的逻辑帧Tick进行权威模拟然后将权威状态位置、速度等发回给所有客户端。如果客户端的预测位置与服务器的权威位置有差异Fusion会自动进行平滑的校正Reconciliation也就是我们常说的“插值”或“微调”而不是生硬的“瞬移”。NetworkTransform组件内部就实现了这一整套逻辑。这就是为什么Fusion能实现如此平滑的同步体验因为它把复杂的预测、回滚和插值逻辑都封装好了。作为开发者你大部分时间只需要关心“我这个物体的哪些属性需要同步用[Networked]标记”和“在FixedUpdateNetwork里根据输入更新状态”。7. 项目结构优化与进阶配置建议一个成功的初始化不仅仅是能让程序跑起来还要为后续的规模开发打下良好的基础。以下是我从实际项目中总结出的几点优化建议。7.1 合理的项目目录结构混乱的Assets文件夹是项目后期的噩梦。建议在引入Fusion之初就建立清晰的目录结构Assets/ ├── Photon/ (由SDK自动生成一般不要动) │ └── Fusion/ ├── Scripts/ │ ├── Network/ │ │ ├── Managers/ (如GameManager, RoomManager) │ │ ├── Behaviours/ (所有继承自NetworkBehaviour的脚本如PlayerBehaviour, WeaponBehaviour) │ │ ├── Data/ (网络数据结构定义如NetworkInputData, PlayerStats) │ │ └── Utilities/ (网络相关工具类) │ └── Common/ (非网络相关的通用脚本) ├── Prefabs/ │ ├── Networked/ (所有需要网络生成的预制体如Player, Enemy, Projectile) │ └── Static/ (静态场景预制体) ├── Scenes/ │ ├── Bootstrap (初始化场景只包含NetworkRunner和全局管理器) │ ├── Lobby (大厅场景) │ └── Gameplay (游戏场景) └── Resources/ (如果需要动态加载) └── FusionInputSettings.asset (输入配置)采用这种结构尤其是将网络相关脚本隔离在Scripts/Network/下能极大提高代码的可维护性。7.2 使用Bootstrap场景模式上面的示例中我们把NetworkRunner放在了游戏场景里。对于正式项目更专业的做法是使用一个独立的初始化场景Bootstrap Scene。创建一个新场景只包含一个GameObject上面挂载一个BootstrapManager脚本和NetworkRunner组件。这个脚本负责初始化Fusion、加载持久化数据、然后根据情况加载大厅或游戏场景。在Unity的File - Build Settings中将这个Bootstrap场景放在Build列表的第一位Index 0。在BootstrapManager的StartGameArgs中将Scene参数设置为你要加载的下一个场景的Build Index例如大厅场景的Index 1。使用NetworkSceneManagerDefault它可以无缝地、在网络同步的方式下加载和卸载场景。这样做的好处是网络连接和核心管理器的生命周期与具体的游戏场景解耦架构更清晰也便于实现“返回大厅”等功能。7.3 输入系统的进阶配置我们之前使用了最简单的内联输入收集。对于复杂的游戏多套控制方案、手柄支持、输入缓冲等你应该充分利用Fusion Input系统。定义Input Actions 在FusionInputSettings资源中不要只依赖默认的“Horizontal/Vertical”。为你游戏中的每个动作创建明确的Action如“Jump”、“Fire”、“Aim”、“Crouch”。创建自定义Input Poller 实现一个类继承自Fusion.InputBehaviour。在这个类中重写PollInput()方法在这里集中收集所有设备的原始输入并填充到你自定义的INetworkInput结构体中。然后将这个InputBehaviour挂载到有NetworkRunner的对象上。结构化输入数据 你的INetworkInput结构体应该清晰反映游戏操作例如包含按钮的按下ButtonSet、释放状态以及视角摇杆的二维向量等。这种方式让输入处理更模块化也便于未来添加新的输入设备或修改键位映射。8. 避坑指南与疑难杂症实录即便按照步骤操作在实际开发中你仍可能遇到一些棘手的问题。以下是我和同事们踩过的一些“坑”以及解决方案。8.1 编译错误“类型‘...’在未引用的程序集中定义”问题描述 导入Fusion SDK后Unity控制台出现大量CS0012错误提示缺少对System.Runtime.CompilerServices.Unsafe等程序集的引用。根本原因 Fusion依赖的一些高级.NET库在项目默认的API兼容性级别下未被自动引用。解决方案确保Edit - Project Settings - Player - Other Settings - Configuration - Api Compatibility Level设置为.NET Standard 2.1。这是最重要的步骤。如果问题依旧可能需要手动编辑项目根目录下的Packages/manifest.json文件在dependencies块中添加显式引用。但通常第一步就能解决99%的问题。8.2 运行时错误“Networked property or RPC accessed outside of fusion simulation”问题描述 在Update()或协程中读取或修改了带有[Networked]标记的属性或者调用了[Rpc]方法导致运行时抛出异常。根本原因[Networked]属性和[Rpc]方法只能在Fusion控制的模拟线程中安全访问即主要在FixedUpdateNetwork()、Spawned()、Render()等回调方法内部。解决方案将所有对[Networked]属性的写入操作移动到FixedUpdateNetwork()中。如果需要在Update()中读取[Networked]属性用于渲染如插值位置这是允许的因为Fusion为渲染提供了线程安全的快照数据。但修改操作绝对不行。将逻辑重构确保状态变更的源头是输入或网络事件并在FixedUpdateNetwork中处理。8.3 物体不同步或位置抖动问题描述 网络物体在客户端间看起来位置不一致或者移动时出现不规则的抖动、跳动。根本原因Tick Rate不匹配 主机和客户端的NetworkProjectConfig中的Tick Rate设置不一致。物理模拟不一致 在FixedUpdateNetwork之外使用了Rigidbody.AddForce等直接影响物理状态的方法导致各客户端物理模拟结果不同。Network Transform配置NetworkTransform组件的插值模式、带宽压缩设置不当。排查与解决确保所有构建版本的NetworkProjectConfig资源尤其是Tick Rate完全相同。所有物理操作如果对象是NetworkObject务必在FixedUpdateNetwork中进行并使用Fusion提供的NetworkRigidbody等组件替代原生物理组件。调整NetworkTransform的Interpolation Data Sources和Interpolation Targets。对于非物理驱动的运动物体通常启用位置和旋转的插值即可。如果带宽紧张可以尝试启用压缩如Half Precision但要注意精度损失。8.4 场景加载失败或对象丢失问题描述 客户端加入后看不到主机已经存在的物体或者场景内容不一致。根本原因场景索引错误StartGameArgs.Scene传入了错误的Build Index。动态生成物体未注册 在主机端动态Spawn的预制体没有在NetworkProjectConfig的Prefab Table中注册对于通过地址ables或资源路径异步加载的预制体有特殊的注册方式。权限问题 物体生成了但生成时没有正确指定输入权威或状态权威。排查与解决仔细核对场景的Build Index并使用SceneManager.GetSceneByBuildIndex进行调试。对于在项目中原位制作的预制体如果通过Runner.Spawn(GameObject prefab)方式生成必须在Prefab Table中注册。注册后Fusion会为预制体分配一个唯一的NetworkObjectGuid网络间通过这个Guid来识别和实例化相同的预制体。理解Runner.Spawn的各个参数特别是PlayerRef inputAuthority参数。谁拥有输入权威谁就能控制这个对象对于玩家角色。对于由服务器权威管理的NPC或道具输入权威可以设为PlayerRef.None。初始化Photon Fusion 2的过程就像是为你的多人游戏搭建了一条高速公路的基石。每一步配置——从账号设置、SDK导入、网络对象创建到脚本编写——都决定了这条公路是否平整、畅通。我个人的体会是初期多花些时间彻底理解NetworkRunner的生命周期、[Networked]属性的工作方式以及FixedUpdateNetwork的意义远比急于实现功能更重要。当你能流畅地跑通第一个双人同步的方块时就意味着你已经掌握了Fusion最核心的运作机制后续添加更复杂的游戏功能无非是在这个稳固的基础上添砖加瓦。如果在后续开发中遇到更具体的问题比如如何处理玩家状态机同步、如何实现延迟补偿射击那又是另一个值得深入探讨的话题了。记住Fusion的官方文档和示例项目是非常宝贵的学习资源遇到难题时多去翻看往往能豁然开朗。