1. 项目概述从零构建一个BRG风格射击游戏最近在社区里看到不少朋友对Unity的BRGBattle Royale Game大逃杀游戏射击玩法感兴趣但感觉入门门槛有点高网上资料要么太零散要么就是纯理论。正好我最近用Unity 2022 LTS版本完整走通了一个小型的BRG射击原型从地图生成到最终战斗结算都实现了。这个项目麻雀虽小五脏俱全非常适合想了解BRG核心机制和Unity中高级特性的开发者。今天我就把这个项目的核心思路、踩过的坑和关键实现细节拆开揉碎了讲给你听目标是让你看完就能动手搭出一个能跑起来的原型。所谓BRG射击核心体验就是“百人竞技最后一人/队生存”。听起来复杂但拆解开来无非是几个模块一个足够大的、带有随机元素的游戏地图一套支持多人或AI模拟多人的网络或本地模拟系统一套公平的武器与战斗规则以及驱动整个游戏进程的“安全区”机制。我们这个教程项目就会聚焦于在Unity单机环境下用AI Bot模拟其他玩家实现一个完整的BRG游戏循环。你会用到Unity的NavMesh系统做AI寻路ScriptableObject做数据配置Addressables管理资源以及一些Shader和粒子特效来提升表现力。无论你是想学习游戏架构设计还是单纯想做一个酷炫的射击Demo这个教程都能给你提供一条清晰的路径。2. 核心系统设计与架构思路做一个BRG游戏最忌讳的就是一开始就埋头写代码。合理的架构能让你后期添加新功能时事半功倍而不是陷入无尽的Bug修复中。我采用的是基于MVC变体的分层架构核心思想是“数据驱动”和“模块解耦”。2.1 整体架构分层解析我把整个项目分为五层从上到下分别是表现层、逻辑层、核心系统层、数据层和底层服务。表现层就是玩家直接看到和听到的包括所有UI用UGUI配合TextMeshPro实现、角色模型动画、武器特效、场景渲染。这一层只负责“展示”不包含任何游戏规则判断。比如一个角色扣血了表现层收到“血量变化”事件然后播放受伤动画、更新血条UI但它不判断“为什么扣血”。逻辑层是游戏规则的大脑。它包含了玩家的状态机站立、移动、瞄准、射击、换弹、倒地等、武器的开火逻辑与伤害计算、安全区的收缩逻辑、物品拾取与使用规则等。这一层是纯C#脚本尽量不依赖特定的MonoBehaviour生命周期方便单元测试。核心系统层提供支撑性的服务。最重要的三个系统是AI行为系统控制Bot的决策如寻路、索敌、攻击、物品生成系统在地图上随机刷出武器、弹药、医疗包、游戏状态管理系统管理游戏流程等待开始、游戏中、结算。这一层像胶水连接着逻辑和表现。数据层是项目的基石我强烈推荐使用ScriptableObject。我为角色属性、武器参数伤害、射速、弹道、后坐力模式、安全区配置、物品属性等都创建了对应的ScriptableObject。这样做的好处是策划或你自己调整数值时无需修改代码在Unity编辑器里拖拖拽拽就能完成而且修改后立即生效非常高效。底层服务包括Unity引擎本身提供的NavMeshAI寻路、物理系统射线检测、碰撞、Input System处理输入以及我们引入的Addressables异步加载资源。确保这些服务被妥善地封装避免上层逻辑直接调用过于底层的API。注意在项目初期就确立好资源加载策略至关重要。对于BRG这种地图大、资源多的类型一定要用Addressables或AssetBundle进行动态加载。千万不要把所有模型、贴图都拖到场景里否则首次打开场景和打包后的体积会让你崩溃。2.2 关键技术选型与考量1. 输入系统放弃旧的Input Manager拥抱新的Input SystemPackage。它支持键鼠、手柄的绑定与重映射处理复合操作如“瞄准射击”更加优雅而且生成的代码清晰易读。这对于需要支持多平台操作的射击游戏来说是必选项。2. AI方案对于上百个Bot性能是关键。Unity自带的NavMeshAgent在寻路上很高效但“决策”逻辑如果写在每个Bot的Update里CPU开销会很大。我的方案是结合状态机和行为树。简单的行为巡逻、逃跑用状态机复杂的决策评估威胁、选择掩体用轻量级的行为树插件如NodeCanvas。同时采用分帧更新策略每帧只更新一部分Bot的AI避免单帧卡顿。3. 网络同步考量为未来扩展虽然当前是单机版但架构要为可能的多人联机留出接口。所有玩家的状态变更位置、血量、装备都通过事件总线如UnityEvent或MessagePipe发布。逻辑层监听这些事件来更新游戏状态。未来如果要改造成网络版只需将事件总线的发布/订阅替换为网络消息的发送/接收即可核心逻辑几乎不用动。4. 渲染与特效BRG地图大Draw Call是性能杀手。要大量使用静态合批、GPU Instancing对于相同的树木、岩石。对于武器开火、命中特效使用对象池管理粒子系统避免频繁的Instantiate和Destroy。后处理方面使用URPUniversal Render Pipeline可以更方便地控制画面风格并集成一些必要的特效如受伤时的屏幕血渍、安全区边界的扭曲效果等。3. 地图与安全区系统的实现BRG游戏的舞台和节奏控制器就是地图与安全区。这部分做得好游戏体验就成功了一半。3.1 程序化地形生成与兴趣点布置完全手搭一个大型地图效率太低且缺乏随机性。我采用程序化生成手工修饰的方式。首先使用Unity的Terrain工具或第三方插件如Gaia生成一个基础的地形网格并应用多层贴图草地、泥土、岩石。然后编写一个MapGenerator脚本负责放置“兴趣点”。兴趣点分为几类建筑群提供丰富的室内战场景、野外掩体岩石、树木、废弃车辆、资源点固定刷高级装备的区域。我会预先制作好这些元素的预制体。// 简化的兴趣点放置逻辑示例 public class MapGenerator : MonoBehaviour { public GameObject[] buildingPrefabs; public GameObject[] coverPrefabs; public int numberOfBuildings 20; public float mapRadius 500f; void Start() { GenerateBuildings(); GenerateCovers(); // ... 生成其他元素 } void GenerateBuildings() { for (int i 0; i numberOfBuildings; i) { // 在随机位置和旋转角度生成建筑 Vector2 randomPos2D Random.insideUnitCircle * mapRadius; Vector3 spawnPosition new Vector3(randomPos2D.x, 0, randomPos2D.y); // 确保生成点在地形上 spawnPosition.y Terrain.activeTerrain.SampleHeight(spawnPosition); Quaternion randomRotation Quaternion.Euler(0, Random.Range(0, 360f), 0); GameObject building Instantiate(buildingPrefabs[Random.Range(0, buildingPrefabs.Length)], spawnPosition, randomRotation); building.transform.parent this.transform; // 保持场景整洁 } } }实操心得在放置建筑和掩体后务必重新烘焙NavMesh。否则AI Bot会在房子里“穿墙”或者卡在石头里。Unity的NavMesh烘焙设置中记得将这些静态物体的Navigation Static勾选上并根据需要调整Walkable区域。3.2 动态安全区机制详解安全区是驱动游戏进程的核心。其逻辑是游戏开始后每隔一段时间随机在上一轮安全区内选择一个点作为新的安全区中心并以一个更小的半径生成新的圆形安全区。留在圈外的玩家会持续掉血。我创建了一个SafeZoneManager单例类来管理这个逻辑。1. 数据结构定义[System.Serializable] public class SafeZoneConfig { public float initialRadius; // 初始安全区半径 public float finalRadius; // 最终决赛圈半径 public int totalPhases; // 总缩圈次数 public float damagePerSecond; // 圈外每秒伤害 public AnimationCurve shrinkCurve; // 控制缩圈速度的曲线 } public class SafeZoneManager : MonoBehaviour { public static SafeZoneManager Instance; public SafeZoneConfig config; public int currentPhase 0; public Vector3 currentCenter; public float currentRadius; private ListVector3 possibleCenters new ListVector3(); // 预计算的可能中心点 // ... 其他字段 }2. 缩圈流程等待阶段显示下一个安全区的范围和中心通常用UI圆环或地面投影特效。收缩阶段安全区边界从旧边界向新边界匀速或变速移动。这个阶段圈外开始造成伤害。稳定阶段安全区在新位置固定一段时间玩家在此战斗。3. 关键实现细节伤害计算在Update中遍历所有玩家包括Bot计算其位置到currentCenter的距离。如果大于currentRadius则调用该玩家的TakeDamage方法传入config.damagePerSecond * Time.deltaTime。可视化安全区的可视化非常重要。我使用一个半透明的Shader球体或圆柱体来表示当前安全区范围。对于“缩圈”的动画可以动态调整这个可视化物体的缩放和位置。下一个安全区的预览可以用一个线框圆环通过LineRenderer组件来显示。随机中心点为了保证游戏性新安全区的中心点不能完全随机。我的策略是在上一轮安全区内预生成一系列“候选点”避开建筑内部、水面等不可到达区域然后从中随机选择。候选点可以通过在安全区内随机采样并用NavMesh.SamplePosition来验证是否在可行走区域上。4. 角色控制与战斗系统这是玩家直接交互的部分手感的好坏决定游戏成败。4.1 第一人称控制器深度定制Unity的标准资源包或Asset Store里的控制器往往不够灵活。我基于CharacterController组件自己写了一个重点优化移动和视角的流畅性。移动逻辑除了前后左右还加入了冲刺、下蹲、跳跃。关键点在于处理好斜坡移动和惯性。CharacterController的SimpleMove方法虽然简单但物理反馈不真实。我使用Move方法并手动计算重力与速度。public class AdvancedFPSController : MonoBehaviour { private CharacterController controller; private Vector3 playerVelocity; private bool groundedPlayer; public float playerSpeed 5.0f; public float jumpHeight 1.0f; public float gravityValue -9.81f; public float sprintMultiplier 1.5f; private bool isSprinting false; void Update() { groundedPlayer controller.isGrounded; if (groundedPlayer playerVelocity.y 0) { playerVelocity.y -0.5f; // 轻微向下的力确保紧贴地面 } // 输入处理 Vector2 moveInput InputSystem.GetAction(Move).ReadValueVector2(); isSprinting InputSystem.GetAction(Sprint).IsPressed(); Vector3 move (transform.right * moveInput.x transform.forward * moveInput.y).normalized; float currentSpeed isSprinting ? playerSpeed * sprintMultiplier : playerSpeed; controller.Move(move * Time.deltaTime * currentSpeed); // 跳跃 if (InputSystem.GetAction(Jump).triggered groundedPlayer) { playerVelocity.y Mathf.Sqrt(jumpHeight * -2f * gravityValue); } // 应用重力 playerVelocity.y gravityValue * Time.deltaTime; controller.Move(playerVelocity * Time.deltaTime); } }视角控制鼠标控制摄像机旋转需要处理上下视角限制防止脖子拧断并加入平滑插值Lerp来消除卡顿感。同时要将摄像机的旋转与角色身体的旋转左右解耦这样在移动时视角可以独立于身体方向实现更灵活的观察。4.2 武器系统从数据配置到射击反馈武器系统是射击游戏的核心乐趣来源。我采用ScriptableObject来定义武器数据实现数据与逻辑分离。1. 武器数据资产WeaponData_SO[CreateAssetMenu(fileName NewWeaponData, menuName BRG/Weapon Data)] public class WeaponData_SO : ScriptableObject { public string weaponName; public GameObject modelPrefab; // 枪械模型 public GameObject bulletPrefab; // 用于弹道预测或特效的预制体非网络同步时 public float damage; public float fireRate; // 每秒发射数 public int magazineSize; public float reloadTime; public float maxSpread; // 最大子弹散布 public float spreadRecoveryRate; // 散布恢复速度 public AnimationCurve recoilPatternX; // 后坐力模式曲线 public AnimationCurve recoilPatternY; public AudioClip fireSound; public GameObject muzzleFlashEffect; }2. 武器逻辑脚本Weapon.cs挂载在玩家手中的武器模型上。开火逻辑使用射线检测Physics.Raycast从摄像机中心发射计算命中点。这是最常用且高效的方式。需要考虑子弹散布根据当前准星稳定度在屏幕中心点周围随机一个偏移量然后从摄像机发射射线。后坐力模拟开火时根据WeaponData_SO中的后坐力曲线为摄像机或武器模型添加一个向上的旋转力和一个左右的随机力。后坐力不是瞬间加上去的而是通过协程Coroutine或每帧累加的方式形成一个有节奏的“上跳”效果并在停止射击后逐渐恢复。换弹与弹药管理维护当前弹匣弹药和总备弹量。换弹动作通过动画事件触发在动画结束时完成弹药数量的逻辑更新。3. 伤害计算与命中反馈伤害计算射线命中后获取命中对象的Hitbox组件我通常为角色不同部位设置不同的Hitbox如头部、身体、四肢。Hitbox上定义了伤害倍率如头部x2.0。最终伤害 武器基础伤害 × 部位倍率 × 可能的距离衰减。反馈击中敌人时在命中点播放一个粒子特效血花或火花并生成一个飘字使用TextMeshPro创建世界空间的UI显示伤害数字。同时通过事件总线发送“玩家受伤”事件UI层监听到后更新敌人的血条显示。踩坑记录射线检测时一定要注意Layer的设置。确保射线只与可命中的层如Player Environment交互忽略掉武器自身、触发器、UI等层。否则会出现“打中自己”或者“隔空打物”的诡异情况。在Physics.Raycast方法中明确指定LayerMask参数是好习惯。5. AI Bot的智能行为与决策用AI模拟其他玩家是单机BRG的关键。目标是让Bot看起来“像真人”而不是傻站着或像僵尸一样冲过来。5.1 基于状态机与行为树的混合AI框架简单的巡逻-攻击-逃跑可以用状态机FSM清晰管理。但BRG中的Bot需要更复杂的决策比如“当前血量低附近有敌人我应该先找掩体还是去找医疗包”、“安全区在缩圈我是直接跑过去还是边打边移动”。这就需要行为树Behavior Tree来定义优先级和条件。我使用一个轻量级的行为树系统其核心节点包括序列节点按顺序执行子节点所有成功才算成功。选择节点按顺序执行子节点直到一个成功。条件节点检查某个条件如“是否看到敌人”、“血量是否低于30%”。动作节点执行具体行为如“移动到某点”、“攻击目标”。一个Bot的顶层行为树可能长这样选择节点主决策 ├── 序列节点紧急情况血量极低且被攻击 │ ├── 条件节点血量 20% 且 正在被攻击 │ └── 动作节点寻找最近掩体并躲避 ├── 序列节点战斗 │ ├── 条件节点看到敌人 │ └── 动作节点评估战局 → 攻击/找掩体对枪 └── 序列节点常规活动 ├── 动作节点检查是否需要跑毒安全区 ├── 动作节点搜索附近物品 └── 动作节点随机巡逻5.2 感知、寻路与战斗行为实现1. 感知系统Bot通过“视觉”和“听觉”感知世界。视觉在Bot前方用一个扇形Physics.OverlapSphere配合角度判断进行检测只检测Player层的物体。同时会发射一条射线到目标如果被墙壁阻挡则判定为“看不到”。这模拟了视野遮挡。听觉当玩家开枪、跑步、换弹时会在事件发生位置创建一个“声音源”游戏对象带有一个逐渐缩小的碰撞体。Bot的感知系统定期检测周围的声音源如果进入范围Bot就会知道“那个方向有动静”并可能前往调查。2. 寻路与移动直接使用NavMeshAgent组件。为Bot设置不同的移动速度行走、跑步、冲刺。关键技巧是动态更新目标。例如当Bot决定“前往安全区”时目标点不是安全区中心而是在安全区边缘NavMesh上的一个随机点这样Bot会沿着道路移动而不是直线穿墙。3. 战斗行为索敌与瞄准一旦通过感知系统发现敌人Bot会将目标位置信息传递给一个“瞄准系统”。这个系统不是让Bot的准星瞬间锁定敌人那太假了而是模拟人类反应有一个从当前位置向目标位置平滑旋转的过程并且会加入一些随机的微小偏移来模拟瞄准抖动。开火策略Bot不会像机枪一样一直开枪。我模拟了“点射”和“压枪”。根据与敌人的距离Bot会选择单发、三连发或扫射。开火时同样会应用武器后坐力数据让Bot的枪口上跳并在停火间隙尝试将准星拉回目标。利用掩体这是提升Bot智商的关键。当Bot受到攻击或决定与敌人交火时它会通过NavMesh.FindClosestEdge寻找附近的掩体位置。移动到掩体后Bot会执行“探头射击”的行为从掩体一侧短暂露出身体进行射击然后缩回去。这通过控制角色胶囊体碰撞器的位置和播放相应的动画来实现。6. 物品、资源与游戏流程管理BRG游戏的资源循环——搜索、争夺、使用——是驱动玩家行动的核心动力。6.1 可交互物品系统设计地图上会刷新武器、弹药、护甲、医疗包等物品。所有可交互物品都继承自一个基类InteractableItem。public abstract class InteractableItem : MonoBehaviour { public string itemName; public Sprite icon; [TextArea] public string description; public float interactionTime 0.5f; // 拾取需要的时间 // 当玩家靠近时显示提示UI public virtual void OnPlayerNearby(Player player) { /* 显示“按F拾取” */ } // 具体的交互逻辑由子类实现 public abstract void Interact(Player player); } public class WeaponItem : InteractableItem { public WeaponData_SO weaponData; public override void Interact(Player player) { // 调用玩家的背包系统尝试拾取武器 player.inventory.TryPickupWeapon(this); // 拾取成功后销毁自身或设置为已拾取状态 Destroy(gameObject); } }物品的生成由ItemSpawner管理。它会在游戏开始时根据预设的生成点通常放在建筑内、空地上随机实例化物品预制体。生成概率可以通过配置表调整比如高级资源点刷出稀有武器的概率更高。6.2 游戏状态机与流程控制整个游戏的生命周期由一个GameManager来管理它本身是一个状态机。public enum GameState { Lobby, Countdown, InGame, SafeZoneShrinking, GameOver } public class GameManager : MonoBehaviour { public static GameManager Instance; public GameState CurrentState { get; private set; } public int playersAlive; // ... 其他引用 void Update() { switch (CurrentState) { case GameState.Lobby: // 等待玩家准备显示大厅UI break; case GameState.Countdown: // 显示倒计时UI初始化所有玩家和Bot break; case GameState.InGame: // 正常游戏逻辑更新安全区计时器等 CheckWinCondition(); break; case GameState.GameOver: // 显示结算界面展示排名和战绩 break; } } public void TransitionToState(GameState newState) { // 退出旧状态 // 进入新状态 CurrentState newState; OnGameStateChanged?.Invoke(newState); // 通知其他系统 } void CheckWinCondition() { if (playersAlive 1) { // 找出幸存者宣布胜利 TransitionToState(GameState.GameOver); } } }GameManager通过事件UnityEvent或C#原生event将状态变化广播出去。SafeZoneManager、UI Manager、Audio Manager等系统监听这些事件做出相应反应。例如当状态变为GameOver时UI管理器弹出结算面板音频管理器播放胜利或失败的音乐。7. 性能优化与常见问题排查当地图上有上百个Bot、大量物品和特效时性能优化是必须面对的挑战。以下是我在项目中总结的关键优化点和排查方法。7.1 核心性能瓶颈分析与优化1. CPU瓶颈AI与逻辑问题上百个Bot每帧都在进行视野检测、决策计算导致主线程卡顿。优化分帧更新将Bot的AI更新分散到多帧中。例如总共100个Bot每帧只更新20个。private ListAIController allBots new ListAIController(); private int updateIndex 0; void Update() { // 每帧更新5个Bot的AI for(int i 0; i 5; i) { if(updateIndex allBots.Count) updateIndex 0; allBots[updateIndex].UpdateAI(); updateIndex; } }降低检测频率Bot的视野检测不用每帧进行可以每0.2-0.5秒检测一次。同样寻路路径的重计算频率也可以降低。使用Jobs System/Burst Compiler对于大量的向量计算、距离判断等可以尝试使用Unity的C# Job System和Burst编译器将这些计算转移到多线程进行能极大提升效率。2. GPU瓶颈渲染问题Draw Call过高复杂的Shader或过多的实时灯光导致帧率下降。优化静态合批将不会移动的场景静态物体建筑、岩石的材质合并标记为StaticUnity会自动进行静态合批。GPU Instancing对于大量相同的物体如树木、草丛使用支持GPU Instancing的Shader和材质球。LOD多层次细节为复杂的角色和建筑模型创建多个细节级别的模型距离远时自动切换到低模。遮挡剔除在Unity的Occlusion Culling面板中烘焙遮挡数据确保摄像机看不到的物体不被渲染。简化后处理谨慎使用屏幕空间反射、环境光遮蔽等昂贵的后处理效果。在移动平台或低配PC上可以考虑关闭或降低质量。3. 内存与资源瓶颈问题场景加载慢运行时内存占用高切换场景卡顿。优化Addressables异步加载所有非启动必需的资源如不同武器的模型、特效、音效都通过Addressables系统异步加载。实现“按需加载”和“资源释放”。对象池对高频创建销毁的对象如子弹特效、血花、飘字UI务必使用对象池。避免Instantiate和Destroy带来的GC垃圾回收压力。7.2 开发与运行时问题速查表在开发过程中你几乎一定会遇到下面这些问题。这里我整理了常见症状、可能原因和排查步骤。问题现象可能原因排查与解决思路Bot卡在某个地方不动1. NavMesh烘焙不完整或有空洞。2. 目标点设置在不可行走区域。3. Bot的碰撞体与其他物体卡住。1. 检查烘焙设置确保所有可行走区域被覆盖障碍物正确标记为Not Walkable。2. 使用NavMesh.SamplePosition来验证目标点是否有效。3. 调整Bot的胶囊体碰撞器大小或检查场景中是否有Collider设置不当的物体。射击射线打不中敌人1. LayerMask设置错误射线忽略了Player层。2. 射线起点或方向计算错误。3. 敌人碰撞体如CapsuleCollider未正确设置。1. 在Physics.Raycast调用处打印或调试LayerMask的值。2. 在Scene视图中使用Debug.DrawRay绘制出射线检查其路径。3. 确认敌人预制体上的Collider组件是否启用大小是否合适。游戏运行一段时间后越来越卡1. 内存泄漏资源未释放。2. 对象池未回收导致对象无限增多。3. 协程或事件监听未正确取消。1. 使用Unity Profiler的Memory模块查看GC Alloc和Managed Heap是否持续增长。2. 检查所有通过Instantiate创建的对象是否都有对应的Destroy或回收到对象池的逻辑。3. 确保在物体销毁OnDestroy时取消所有由其启动的协程并注销其订阅的事件。安全区伤害不生效或错误生效1. 玩家位置判断逻辑有误未考虑Y轴。2. 伤害计算未乘以Time.deltaTime导致单帧伤害过高。3.SafeZoneManager的更新顺序晚于玩家状态更新。1. 计算距离时确保使用的是在水平面XZ平面上的距离或者包含Y轴但安全区是一个圆柱体。2. 确认伤害应用代码为player.health - damagePerSecond * Time.deltaTime;。3. 在脚本执行顺序Edit - Project Settings - Script Execution Order中确保SafeZoneManager在玩家状态更新之前执行。武器换弹动画播放但弹药数没变动画事件未正确触发逻辑代码。1. 在武器换弹动画的特定帧上添加动画事件。2. 在动画事件调用的函数中确保执行了增加弹药的逻辑如currentMagazine magazineSize;。3. 使用调试器或打印日志确认动画事件函数是否被调用。打包后运行时出现粉色材质Missing Shader使用了编辑器特有的Shader或Shader变体未正确打包。1. 检查粉色材质球使用的Shader确保其在目标平台如Windows Android是支持的。2. 如果是URP/ HDRP项目确保已正确配置管线并且所有材质球都使用了对应的Lit Shader。3. 在Graphics Settings中将缺失的Shader添加到Always Included Shaders列表中。一个高级调试技巧自定义游戏内调试面板。我习惯创建一个DebugPanel通过快捷键如F3呼出里面可以实时显示当前帧率、Bot数量、玩家坐标、游戏状态等信息甚至可以动态修改一些参数如游戏速度、伤害倍率。这在排查复杂问题时非常有用。你可以使用IMGUI快速实现一个这样的面板。最后性能优化是一个持续的过程。永远相信Profiler数据而不是你的感觉。在开发的关键节点定期使用Unity Profiler特别是Deep Profile模式来定位性能热点有针对性地进行优化。先从CPU和GPU占用最高的部分下手往往能取得事半功倍的效果。这个BRG射击项目虽然只是一个原型但涵盖了从架构设计到具体实现从玩法逻辑到性能调优的完整链条。希望这份超详细的拆解能帮你理清思路少走弯路。在实际动手时最宝贵的经验往往来自于解决那些意料之外的Bug所以别怕出错多尝试多总结。