1. 项目概述:为什么鼠标悬停效果是游戏交互的灵魂
在Unity3D游戏开发中,交互反馈是连接玩家与虚拟世界的桥梁。一个流畅、直观且富有吸引力的交互反馈,能极大提升游戏的可玩性和沉浸感。而鼠标悬停效果,作为最基础也是最频繁的交互反馈之一,其重要性不言而喻。想象一下,当玩家将鼠标移到一个可交互的按钮、道具或角色上时,如果对象毫无反应,玩家会感到困惑:“这东西能点吗?”;反之,如果对象能立即给予一个放大、高亮或轻微晃动的视觉反馈,玩家瞬间就能理解其可交互性,操作信心大增。
这次我们要深入探讨的,就是利用Transform.localScale这个看似简单的属性,来实现一套酷炫且性能高效的鼠标悬停效果。你可能会问,实现悬停效果不是有UI系统的Button组件吗?或者用Shader做高亮不是更酷?没错,但对于很多特定场景,比如3D场景中的模型交互、自定义UI元素、或者追求极致轻量和可控性的情况,直接操控Transform.localScale具有不可替代的优势。它不依赖特定的UI系统,不涉及材质球切换,纯粹通过改变物体的局部缩放来产生视觉变化,计算开销极小,实现逻辑清晰,非常适合新手理解射线检测与物体交互的基本原理,同时也是老手进行快速原型开发和高性能优化的利器。
围绕“Unity3D游戏开发:如何用Transform.localScale实现酷炫的鼠标悬停效果”这个核心,我们将不仅完成一个“能用的”效果,更要拆解其背后的设计思路、实现细节、性能考量以及如何将其变得“酷炫”。我们会从最基础的射线检测讲起,逐步深入到动画曲线、多对象管理、性能优化等实战技巧,并附上完整的、可直接复用的代码。无论你是刚接触Unity的初学者,还是想寻找一种轻量级交互解决方案的开发者,这篇文章都将为你提供一条清晰的路径。
2. 核心思路与方案设计:为何选择LocalScale?
在动手写代码之前,我们先要厘清设计思路。为什么在众多实现悬停效果的方法中,我们要聚焦于Transform.localScale?
2.1 方案对比与选型理由
实现鼠标悬停视觉反馈,常见的有以下几种方法:
- 改变材质颜色/属性:通过脚本访问
Renderer.material,修改其颜色或_Emission等属性实现高亮。这种方法效果丰富,但涉及材质实例化(material属性会创建实例),可能增加Draw Call,且对性能敏感的场景需要谨慎管理。 - Outline/高亮Shader:使用后处理或单独的高亮Shader。效果最炫酷,能实现发光、描边等高级效果,但对Shader编程有一定要求,且后处理方案有全局性能开销。
- UI Button组件的状态过渡:对于UGUI系统,
Button组件内置了Transition选项(颜色、精灵、动画),配置简单但仅限于UI元素。 - 改变Transform.localScale:直接修改物体的局部缩放值。其核心优势在于极度轻量和通用。它不创建新的材质实例,不依赖特定的渲染管线或UI系统,只是一个简单的向量值变化。配合动画系统或插值运算,可以轻松实现平滑的放大、缩小、脉冲等效果。
选择localScale的核心理由可以总结为三点:
- 性能优先:一次缩放变换的计算开销远低于材质切换或Shader运算,特别适合移动端或对象数量众多的场景。
- 实现简单:逻辑直观,易于理解和调试,是学习游戏对象交互的绝佳起点。
- 效果灵活:缩放不仅可以模拟“凸起”被选中的感觉,结合非均匀缩放(如只放大X、Y轴)或动画曲线,能创造出弹跳、挤压等多种动态效果。
2.2 整体架构设计
我们的目标系统需要可靠地完成以下任务:
- 检测:准确判断鼠标光标是否悬停在一个特定的游戏对象上。
- 响应:当悬停发生时,触发该对象的缩放动画;当鼠标移开时,触发恢复动画。
- 管理:优雅地处理多个可交互对象,确保同一时间只有一个对象处于“悬停”状态,或者支持多个对象同时独立响应。
- 美化:让缩放动画看起来自然、流畅、有吸引力,而不是生硬地跳变。
基于此,我们将系统拆解为几个核心模块:
- 输入检测模块:每帧利用
Camera.ScreenPointToRay将鼠标屏幕坐标转换为一条射线,并通过Physics.Raycast检测与带有碰撞器的物体的交点。 - 效果控制模块:挂载在单个可交互物体上,负责管理自身的缩放状态(正常、悬停中、恢复中)和执行缩放动画。
- 动画驱动模块:使用协程(
Coroutine)配合插值函数(Mathf.Lerp)或UnityEngine.Animation组件,来实现平滑的缩放值过渡。我们将重点讲解协程方式,因为它更轻量、更可控。 - 可选全局管理模块:一个中心化的管理器,用于处理对象之间的悬停互斥逻辑,或者统一管理动画参数。
这个架构确保了功能的解耦,每个脚本职责单一,便于后续扩展和维护。例如,未来你可以很容易地将缩放效果替换为颜色变化效果,而无需重写检测逻辑。
3. 关键技术细节与实现原理
理解了整体设计,我们来深入每个环节的技术细节。这是将想法转化为代码的关键一步。
3.1 射线检测:鼠标如何“触摸”到3D世界?
在3D游戏中,鼠标的屏幕坐标是一个2D点。要让这个点与3D空间中的物体交互,就需要一座桥梁——射线检测。
原理:从摄像机所在位置,穿过鼠标在屏幕上的那个像素点,向游戏世界内部发射一条无限延伸的射线。这条射线就是鼠标在3D空间中的“指针”。我们通过物理系统检测这条射线是否与任何碰撞体相交。
关键代码与解析:
// 在每帧更新的函数中,例如 Update() Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hitInfo; // 用于存储射线命中的详细信息 // 发射射线,最大距离设为100,忽略某些图层可按需设置 if (Physics.Raycast(ray, out hitInfo, 100f)) { // hitInfo.collider.gameObject 就是被鼠标“指到”的游戏对象 GameObject hoveredObject = hitInfo.collider.gameObject; // 接下来就可以对这个对象进行操作了 }注意:这里使用了
Camera.main。在正式项目中,尤其是有多摄像机的场景(如UI相机和场景相机分离),最好通过FindObjectOfType或序列化字段赋值的方式明确指定用于射线检测的摄像机,避免依赖标签带来的不确定性。
为什么需要Collider?射线检测依赖物理系统,而物理系统只与Collider组件交互。因此,任何希望被鼠标悬停检测到的物体,必须附加至少一个Collider(如BoxCollider,SphereCollider,MeshCollider)。对于复杂的模型,使用MeshCollider最精确但性能开销大,通常用简单的BoxCollider或CapsuleCollider进行近似包裹以优化性能。
3.2 Transform.localScale 的奥秘
Transform.localScale是一个Vector3类型,代表物体相对于其父节点的缩放比例。如果物体没有父节点,那么localScale就是其在世界空间中的缩放。
关键特性:
- 局部性:
localScale是局部坐标系的缩放。这意味着如果一个物体父节点的缩放是 (2,2,2),该物体自身的localScale是 (0.5,0.5,0.5),那么它在世界空间中看起来的尺寸是 1(2 * 0.5)。 - 非均匀缩放:三个轴(x, y, z)可以独立设置。这为我们创造有趣效果提供了可能,比如悬停时只拉长高度(y轴),模拟一个按钮被按下的感觉。
- 与LossyScale的区别:
Transform.lossyScale是只读的,表示物体在世界空间中的最终缩放(考虑了所有父节点的缩放)。在修改缩放时,我们总是操作localScale。
在悬停效果中的应用:我们通常会定义两个关键的Vector3值:
normalScale:物体正常状态下的缩放,通常是Vector3.one(1,1,1)。hoverScale:物体悬停时的目标缩放,例如Vector3.one * 1.2f,表示整体放大20%。
动画的过程,就是让localScale从normalScale平滑地过渡到hoverScale,或反向过渡。
3.3 平滑动画的实现:协程与插值
直接在一帧内将localScale设置为目标值会产生“跳变”感,体验生硬。平滑动画的核心是插值。
线性插值:Mathf.Lerp和Vector3.Lerp是 Unity 中最常用的插值函数。它们根据一个在0到1之间的比例因子t,计算两个值之间的中间值。
// 计算 currentScale 到 targetScale 之间,进度为 t 时的值 currentScale = Vector3.Lerp(startScale, targetScale, t); transform.localScale = currentScale;如何让t随时间变化?这就是协程大显身手的地方。协程允许我们将一个任务分散到多帧中执行,非常适合制作动画。
基础动画协程框架:
IEnumerator ScaleAnimation(Vector3 targetScale, float duration) { Vector3 startScale = transform.localScale; float elapsedTime = 0f; while (elapsedTime < duration) { // 计算当前进度(0到1之间) float t = elapsedTime / duration; // 使用平滑函数处理t,可以让动画更有弹性,下文会详述 // float smoothT = Mathf.SmoothStep(0f, 1f, t); transform.localScale = Vector3.Lerp(startScale, targetScale, t); elapsedTime += Time.deltaTime; // 计时器递增 yield return null; // 等待下一帧 } // 确保最终精确设置为目标值 transform.localScale = targetScale; }调用方式:StartCoroutine(ScaleAnimation(hoverScale, 0.2f));
这个简单的循环,就是所有平滑缩放动画的心脏。通过控制duration(持续时间)和插值方式,我们可以创造出各种速度感。
4. 完整实现步骤与代码解析
现在,我们将所有模块组合起来,创建一个完整的、可复用的悬停效果脚本。我将提供两个版本:一个基础单对象版,一个增强带管理版。
4.1 基础单对象实现
这个脚本直接挂载在需要悬停效果的物体上,独立工作。
using UnityEngine; using System.Collections; [RequireComponent(typeof(Collider))] // 确保物体有碰撞器 public class HoverEffectBasic : MonoBehaviour { [Header("缩放设置")] [Tooltip("悬停时的目标缩放比例")] public Vector3 hoverScale = new Vector3(1.2f, 1.2f, 1.2f); [Tooltip("缩放动画的持续时间(秒)")] public float scaleDuration = 0.15f; [Header("动画曲线")] [Tooltip("使用动画曲线让缩放更自然。默认是线性。")] public AnimationCurve scaleCurve = AnimationCurve.Linear(0, 0, 1, 1); private Vector3 originalScale; private Coroutine currentScaleCoroutine; void Start() { originalScale = transform.localScale; // 确保碰撞器是触发状态,如果不需要物理碰撞的话 // GetComponent<Collider>().isTrigger = true; } void OnMouseEnter() { // 鼠标进入时,播放放大动画 if (currentScaleCoroutine != null) { StopCoroutine(currentScaleCoroutine); } currentScaleCoroutine = StartCoroutine(AnimateScale(hoverScale, scaleDuration)); } void OnMouseExit() { // 鼠标离开时,播放恢复动画 if (currentScaleCoroutine != null) { StopCoroutine(currentScaleCoroutine); } currentScaleCoroutine = StartCoroutine(AnimateScale(originalScale, scaleDuration)); } IEnumerator AnimateScale(Vector3 targetScale, float duration) { Vector3 startScale = transform.localScale; float time = 0f; while (time < duration) { float t = time / duration; // 线性进度 0~1 float evaluatedT = scaleCurve.Evaluate(t); // 应用动画曲线 transform.localScale = Vector3.Lerp(startScale, targetScale, evaluatedT); time += Time.deltaTime; yield return null; } transform.localScale = targetScale; // 确保最终值准确 currentScaleCoroutine = null; } // 提供一个公共方法,方便其他脚本手动触发或重置 public void ResetToOriginalScale() { if (currentScaleCoroutine != null) StopCoroutine(currentScaleCoroutine); transform.localScale = originalScale; } }代码要点解析:
[RequireComponent(typeof(Collider))]:这是一个特性,会自动为游戏对象添加一个Collider组件(如果尚未拥有),防止我们忘记添加必要的碰撞器。OnMouseEnter与OnMouseExit:这是 Unity 内置的、基于物理射线检测的消息函数。当鼠标射线进入或离开该物体的碰撞体时,它们会被自动调用。这种方法极其简单,但有一个重要限制:它要求场景中只有一个摄像机,并且该摄像机的Physics Raycaster设置正确(对于3D物体,主摄像机默认就有)。对于复杂的UI与3D物体混合的场景,可能需要更通用的射线检测方法。- 协程管理:我们使用
currentScaleCoroutine来存储当前正在运行的动画协程。在开始新动画前,先停止旧的协程。这可以防止在鼠标快速进出时,多个动画协程同时运行导致缩放值混乱。 - 动画曲线:
AnimationCurve是一个强大的工具。默认的Linear是匀速运动。你可以在 Inspector 窗口中编辑这条曲线,轻松创建出“缓入缓出”(Ease-in-out)、“弹性”(Bounce)等效果,让动画更具动感,而无需修改代码。
4.2 增强版:支持射线检测与全局管理
基础版虽然简单,但OnMouseEnter/Exit在某些复杂场景下可能不可靠。下面我们实现一个更健壮、更通用的版本,它使用自定义的射线检测,并且可以方便地扩展为全局管理器。
步骤一:创建悬停效果组件
using UnityEngine; using System.Collections; public class AdvancedHoverEffect : MonoBehaviour { public Vector3 hoverScaleMultiplier = new Vector3(1.15f, 1.15f, 1.15f); public float animationDuration = 0.2f; public AnimationCurve animationCurve = AnimationCurve.EaseInOut(0,0,1,1); private Vector3 originalScale; private bool isHovering = false; private Coroutine scaleRoutine; void Start() { originalScale = transform.localScale; } // 被管理器调用的方法 public void OnHoverStart() { if (isHovering) return; isHovering = true; StartScaleAnimation(originalScale, Vector3.Scale(originalScale, hoverScaleMultiplier)); } public void OnHoverEnd() { if (!isHovering) return; isHovering = false; StartScaleAnimation(transform.localScale, originalScale); } private void StartScaleAnimation(Vector3 from, Vector3 to) { if (scaleRoutine != null) StopCoroutine(scaleRoutine); scaleRoutine = StartCoroutine(ScaleRoutine(from, to, animationDuration)); } IEnumerator ScaleRoutine(Vector3 from, Vector3 to, float dur) { float elapsed = 0f; while (elapsed < dur) { float t = elapsed / dur; transform.localScale = Vector3.Lerp(from, to, animationCurve.Evaluate(t)); elapsed += Time.deltaTime; yield return null; } transform.localScale = to; scaleRoutine = null; } }步骤二:创建全局鼠标悬停管理器这个管理器挂在场景中一个空物体上(如“GameManager”),负责每帧进行射线检测,并通知命中的物体。
using UnityEngine; using System.Collections.Generic; public class MouseHoverManager : MonoBehaviour { public Camera raycastCamera; // 指定用于射线检测的摄像机 public LayerMask interactableLayer; // 可交互物体所在的图层,用于过滤 [Tooltip("悬停时是否允许同时高亮多个物体?")] public bool allowMultipleHovers = false; private AdvancedHoverEffect lastHoveredObject; private List<AdvancedHoverEffect> currentHoveredObjects = new List<AdvancedHoverEffect>(); void Start() { if (raycastCamera == null) raycastCamera = Camera.main; } void Update() { HandleMouseHover(); } void HandleMouseHover() { Ray ray = raycastCamera.ScreenPointToRay(Input.mousePosition); RaycastHit hit; // 单物体悬停模式 if (!allowMultipleHovers) { AdvancedHoverEffect newHover = null; if (Physics.Raycast(ray, out hit, Mathf.Infinity, interactableLayer)) { newHover = hit.collider.GetComponent<AdvancedHoverEffect>(); } // 如果当前悬停的对象和上一帧不一样 if (newHover != lastHoveredObject) { // 结束上一个对象的悬停 if (lastHoveredObject != null) { lastHoveredObject.OnHoverEnd(); } // 开始新对象的悬停 if (newHover != null) { newHover.OnHoverStart(); } lastHoveredObject = newHover; } } // 多物体悬停模式(例如,鼠标划过一堆物体) else { // 清空当前列表,并结束所有之前物体的悬停 foreach (var obj in currentHoveredObjects) { if (obj != null) obj.OnHoverEnd(); } currentHoveredObjects.Clear(); // 检测所有命中的物体 RaycastHit[] hits = Physics.RaycastAll(ray, Mathf.Infinity, interactableLayer); foreach (var hitInfo in hits) { var hoverEffect = hitInfo.collider.GetComponent<AdvancedHoverEffect>(); if (hoverEffect != null) { hoverEffect.OnHoverStart(); currentHoveredObjects.Add(hoverEffect); } } } } }这个增强版方案的优势:
- 灵活性:你可以指定任何摄像机进行射线检测,完美适配多相机场景。
- 性能优化:通过
LayerMask过滤,只对特定图层的物体进行检测,避免不必要的射线计算。 - 集中管理:所有悬停逻辑集中在一个管理器里,方便统一修改行为(如悬停音效、全局开关)。
- 模式可选:提供了“单物体”和“多物体”两种悬停模式,适应不同需求。
5. 进阶技巧与效果增强
实现了基础功能后,我们可以让效果变得更“酷炫”。这里分享几个实用的进阶技巧。
5.1 使用动画曲线创造丰富动效
在 Inspector 中编辑AnimationCurve是提升效果最快捷的方式。不要只满足于线性插值。
- 缓入缓出:
AnimationCurve.EaseInOut(0,0,1,1)。这是最常用的曲线,动画在开始和结束时慢,中间快,感觉更自然。 - 弹性效果:创建一个在终点附近有轻微回弹的曲线。你可以手动拖拽曲线,或者使用代码
AnimationCurve的AddKey方法来模拟。这种效果让物体缩放时像有橡皮筋一样,充满活力。 - 脉冲效果:让物体在悬停时持续有节奏地轻微缩放。这需要修改动画协程的逻辑,使其在
hoverScale和originalScale之间循环。
脉冲效果代码示例:
IEnumerator PulsateEffect(Vector3 baseScale, float pulseAmount, float pulseSpeed) { while (isHovering) // 假设有一个标志位控制脉冲是否继续 { // 使用正弦函数产生周期性的值 float t = Mathf.Sin(Time.time * pulseSpeed) * 0.5f + 0.5f; // 将值映射到0~1 Vector3 target = Vector3.Lerp(baseScale, baseScale * pulseAmount, t); transform.localScale = target; yield return null; } // 退出脉冲时,平滑缩回原始大小 yield return StartCoroutine(ScaleRoutine(transform.localScale, baseScale, 0.3f)); }5.2 组合其他效果
缩放可以与其他效果叠加,产生更强烈的反馈。
- 颜色变化:在缩放动画的同时,使用
MaterialPropertyBlock来动态修改物体的颜色或自发光强度,避免创建新的材质实例。private MaterialPropertyBlock mpb; private Renderer objectRenderer; void Start() { objectRenderer = GetComponent<Renderer>(); mpb = new MaterialPropertyBlock(); objectRenderer.GetPropertyBlock(mpb); } void UpdateColorDuringHover(Color targetColor, float t) { mpb.SetColor("_Color", Color.Lerp(Color.white, targetColor, t)); // 如果是URP/HDRP,属性名可能是 "_BaseColor" objectRenderer.SetPropertyBlock(mpb); } - 轻微旋转:在悬停时,让物体沿某个轴轻微来回旋转,模拟“待机”或“被吸引”的感觉。
- 阴影或UI提示:当物体悬停时,实例化一个简单的UI文本或一个投影粒子,显示物品名称或提示信息。
5.3 性能优化要点
当场景中有成百上千个可交互物体时,性能变得至关重要。
- 减少射线检测开销:
- 使用图层:如上文所述,为所有可交互物体设置一个专用图层(如“Interactable”),射线检测时指定该图层,避免检测无关物体。
- 降低检测频率:如果不是需要极高实时性,可以将射线检测放在
FixedUpdate中,或者每N帧检测一次。 - 使用非触发式碰撞器:
OnMouseEnter在内部也使用了射线检测。对于简单形状,使用BoxCollider而非MeshCollider。
- 优化动画更新:
- 对象池化管理:如果物体是动态生成和销毁的(如背包中的物品),考虑使用对象池复用
AdvancedHoverEffect组件,避免频繁的GetComponent调用。 - 按需更新:在动画协程中,
yield return null每帧都会执行。如果动画接近尾声,可以提前结束协程。
- 对象池化管理:如果物体是动态生成和销毁的(如背包中的物品),考虑使用对象池复用
- 对于UI元素:如果是对UGUI元素做悬停,强烈建议使用Unity自带的
Selectable(Button、Toggle等的基类)的过渡功能,或者使用EventTrigger组件配合IPointerEnterHandler和IPointerExitHandler接口,这比用3D射线检测UI效率高得多。
6. 常见问题与实战调试技巧
在实际开发中,你肯定会遇到一些问题。这里列出一些典型问题及其解决方案。
6.1 问题排查清单
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 鼠标悬停无任何反应 | 1. 物体没有Collider组件。2. 物体的 Collider被禁用或isTrigger状态不符合预期。3. 摄像机没有正确设置( OnMouseEnter方式)。4. 物体图层不在射线检测的 LayerMask内(管理器方式)。5. 有其他UI元素(如全屏透明Image)挡住了射线。 | 1. 检查并添加Collider。2. 检查 Collider组件的勾选状态和isTrigger。3. 确保用于检测的摄像机是激活的,并且其 Physics Raycaster(对UI)或本身(对3D物体)正常工作。4. 在管理器中检查 interactableLayer设置,确保物体图层被包含。5. 检查UI Canvas的 Graphic Raycaster和渲染模式。 |
| 缩放动画卡顿或不流畅 | 1. 动画协程逻辑有误,可能用了WaitForSeconds而不是每帧插值。2. 在 Update中同时用Mathf.Lerp和协程动画,导致双重控制。3. 游戏整体帧率过低。 | 1. 确保动画协程使用while循环配合Time.deltaTime和yield return null。2. 确保缩放值只在一个地方被修改(要么在协程,要么在 Update,不要同时)。3. 使用性能分析器(Profiler)查看CPU开销。 |
| 多个物体同时被悬停 | 1. 使用了Physics.RaycastAll但未正确处理。2. 多个物体的 Collider重叠。 | 1. 如果只需要一个物体,使用Physics.Raycast获取第一个命中的即可。2. 调整碰撞体大小或位置,避免不必要的重叠。使用 Physics.Raycast并指定maxDistance也有帮助。 |
| 缩放中心点不对 | 物体的轴心点(Pivot)不在几何中心。 | 在3D建模软件中调整模型的轴心点,或者在Unity中创建一个空物体作为父节点,将模型作为子对象,对父物体进行缩放。 |
| 动画曲线不生效 | 在协程中忘记对进度t使用AnimationCurve.Evaluate(t)。 | 检查AnimateScale协程,确保t经过了曲线求值:float evaluatedT = scaleCurve.Evaluate(t); |
6.2 实战心得与技巧
- 关于
OnMouseEnter的局限性:在移动平台上,或者当场景中有多个摄像机处理不同渲染层时,OnMouseEnter可能无法触发。对于需要跨平台或复杂场景的项目,建议从一开始就采用自定义射线检测的管理器模式,它给你更多的控制权。 - 使用
MaterialPropertyBlock修改颜色:这是一个非常重要的优化技巧。直接修改renderer.material.color会创建该材质的唯一副本,如果1000个物体都这样做,就会产生1000个材质实例,极大增加Draw Call。而MaterialPropertyBlock允许你修改渲染属性而不创建新的材质,性能开销小得多。 - 为动画添加音效:在
OnHoverStart和OnHoverEnd方法中,可以添加AudioSource.PlayOneShot()来播放轻微的悬停音效,能极大增强交互质感。注意使用对象池管理音频源以避免性能问题。 - 在编辑器中进行调试:在
AdvancedHoverEffect脚本的OnHoverStart/End方法里临时添加Debug.Log(gameObject.name);,可以非常直观地在控制台看到哪个物体被悬停了,这对于调试复杂的交互逻辑非常有用。 - 考虑使用
DoTween或LeanTween:如果你在项目中已经使用了这些流行的动画插件,用它们来实现缩放动画会更加简洁。例如,用DoTween只需一行代码:transform.DOScale(hoverScale, duration).SetEase(Ease.OutBack);。插件内部处理了动画曲线、循环、回调等复杂逻辑,可以节省大量开发时间。但理解本文中的原生实现原理,能让你更好地使用和调试这些插件。
通过以上从原理到实现,从基础到进阶的完整拆解,你应该已经掌握了使用Transform.localScale制作高质量鼠标悬停效果的全部技能。记住,好的交互效果是细心打磨出来的,多尝试不同的动画曲线、缩放比例和组合效果,找到最适合你游戏风格的那一种。