十一、Unity 新特性 十一、Unity 新特性 1. 可编程渲染管线 (Scriptable Render Pipeline, SRP) 可编程渲染管线 (SRP) 是 Unity 引擎渲染架构的一次重大革新。它允许开发者不再局限于内置渲染管线,而是可以根据项目需求自定义渲染管线。SRP 提供了基础框架,开发者可以使用 C# 脚本和 Shader Graph 来自定义渲染流程,实现各种高级渲染效果和优化策略。SRP 主要包含两种预设管线:通用渲染管线 (Universal Render Pipeline, URP) 和高清渲染管线 (High Definition Render Pipeline, HDRP)。
十一、Unity 新特性
1. 可编程渲染管线 (Scriptable Render Pipeline, SRP)
可编程渲染管线 (SRP) 是 Unity 引擎渲染架构的一次重大革新。它允许开发者不再局限于内置渲染管线,而是可以根据项目需求自定义渲染管线。SRP 提供了基础框架,开发者可以使用 C# 脚本和 Shader Graph 来自定义渲染流程,实现各种高级渲染效果和优化策略。SRP 主要包含两种预设管线:通用渲染管线 (Universal Render Pipeline, URP) 和高清渲染管线 (High Definition Render Pipeline, HDRP)。
通用渲染管线 (URP):URP 旨在提供高性能、可扩展的渲染方案,适用于广泛的平台,包括移动设备、Web 和主机平台。它在性能和画面质量之间取得了良好的平衡,是移动游戏和轻量级应用的首选。
高清渲染管线 (HDRP):HDRP 专注于实现电影级别的视觉效果,适用于高端平台,例如 PC 和主机平台。它提供了诸如延迟渲染、体积光照、屏幕空间反射等高级渲染特性,能够呈现逼真、细腻的游戏画面。
1.1 代码实践:使用 URP 创建自定义渲染效果
以下代码示例展示了如何使用 URP 和 Shader Graph 创建一个简单的自定义后处理效果,例如颜色调整。
// 创建一个新的 Shader Graph 资源,命名为 ColorAdjustPostProcess.shadergraph // 在 Shader Graph 中,创建一个 Color Adjust 节点,连接到 Fragment 输出 // 创建一个 C# 脚本,命名为 ColorAdjustPostProcessRendererFeature.cs using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class ColorAdjustPostProcessRendererFeature : ScriptableRendererFeature { [System.Serializable] public class ColorAdjustSettings { public RenderPassEvent renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; public Material colorAdjustMaterial = null; [Range(0f, 1f)] public float saturation = 1f; [Range(0f, 1f)] public float brightness = 0.5f; [Range(0f, 1f)] public float contrast = 0.5f; } public ColorAdjustSettings settings = new ColorAdjustSettings(); private ColorAdjustPostProcessPass colorAdjustPass; public override void Create() { colorAdjustPass = new ColorAdjustPostProcessPass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.colorAdjustMaterial == null) { Debug.LogWarningFormat("Missing Color Adjust Material. Post Process Pass will not be added."); return; } colorAdjustPass.renderPassEvent = settings.renderPassEvent; renderer.EnqueuePass(colorAdjustPass); } class ColorAdjustPostProcessPass : ScriptableRenderPass { private Material material; private ColorAdjustSettings settings; private RenderTargetIdentifier source; private RenderTargetIdentifier destination; private string profilerTag; public ColorAdjustPostProcessPass(ColorAdjustSettings settings) { this.settings = settings; this.material = settings.colorAdjustMaterial; this.profilerTag = "Color Adjust Post Process"; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { source = renderingData.cameraData.renderer.cameraColorTarget; RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor; descriptor.depthBufferBits = 0; cmd.GetTemporaryRT(ShaderIDs._TempColorTexture, descriptor, FilterMode.Bilinear); destination = new RenderTargetIdentifier(ShaderIDs._TempColorTexture); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (material == null) return; CommandBuffer cmd = CommandBufferPool.Get(profilerTag); using (new ProfilingScope(cmd, new ProfilingSampler(profilerTag))) { material.SetFloat("_Saturation", settings.saturation); material.SetFloat("_Brightness", settings.brightness); material.SetFloat("_Contrast", settings.contrast); Blit(cmd, source, destination, material); Blit(cmd, destination, source); // 将后处理结果 blit 回 source } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void OnCameraCleanup(CommandBuffer cmd) { cmd.ReleaseTemporaryRT(ShaderIDs._TempColorTexture); } } }
// ShaderIDs.cs 用于存储 Shader 属性的 ID,避免字符串查找的性能开销 public static class ShaderIDs { public static readonly int _MainTex = Shader.PropertyToID("_MainTex"); public static readonly int _Saturation = Shader.PropertyToID("_Saturation"); public static readonly int _Brightness = Shader.PropertyToID("_Brightness"); public static readonly int _Contrast = Shader.PropertyToID("_Contrast"); public static readonly int _TempColorTexture = Shader.PropertyToID("_TempColorTexture"); }
内容详解:
Shader Graph 创建后处理材质: 首先,我们使用 Shader Graph 创建了一个名为 ColorAdjustPostProcess.shadergraph 的 Shader Graph 资源。在这个 Shader Graph 中,我们添加了 Color Adjust 节点,并将其连接到 Fragment 输出。Color Adjust 节点允许我们在 Shader 中调整图像的饱和度、亮度、对比度等参数。
创建 Renderer Feature 脚本: ColorAdjustPostProcessRendererFeature.cs 脚本继承自 ScriptableRendererFeature,用于将自定义的后处理 Pass 注入到 URP 的渲染流程中。
ColorAdjustSettings 类定义了后处理效果的参数,包括 RenderPassEvent(渲染 Pass 的注入时机)、colorAdjustMaterial(后处理材质)、以及饱和度、亮度、对比度等参数。
Create() 方法创建了 ColorAdjustPostProcessPass 实例。
AddRenderPasses() 方法将 ColorAdjustPostProcessPass 注入到渲染器的渲染队列中。
创建 Renderer Pass 脚本: ColorAdjustPostProcessPass 类继承自 ScriptableRenderPass,负责实际的后处理逻辑。
OnCameraSetup() 方法在相机渲染设置阶段被调用,用于获取渲染目标纹理,并创建临时渲染纹理。
Execute() 方法是渲染 Pass 的核心,它在指定的 RenderPassEvent 时机被调用。在这个方法中,我们:
从 CommandBufferPool 获取一个 Command Buffer,用于记录渲染命令。
使用 ProfilingScope 标记性能分析区域。
将 ColorAdjustSettings 中的参数传递给后处理材质。
使用 Blit() 函数执行后处理操作。Blit() 函数将源纹理 (source) 拷贝到目标纹理 (destination),并应用指定的材质。这里我们先将源纹理 Blit 到临时纹理,应用后处理效果,然后再将临时纹理 Blit 回源纹理,实现后处理效果的叠加。
OnCameraCleanup() 方法在相机渲染清理阶段被调用,用于释放临时渲染纹理。
ShaderIDs 脚本: ShaderIDs.cs 脚本定义了 Shader 属性的 ID,使用 Shader.PropertyToID() 方法将 Shader 属性名称转换为唯一的 ID 整数。这样做可以避免在运行时进行字符串查找,提高性能。
使用 Renderer Feature: 要使用这个自定义后处理效果,需要在 URP Renderer 资源中添加 ColorAdjustPostProcessRendererFeature。在 Inspector 面板中,可以配置 Color Adjust Material 为我们创建的 ColorAdjustPostProcess.shadergraph 材质,并调整饱和度、亮度、对比度等参数。
1.2 Mermaid 图表:URP 渲染管线流程
内容详解:
Camera (相机): 渲染流程的起点,相机决定了场景中哪些物体需要被渲染。
Culling (裁剪): 剔除相机视野之外的物体,以及被遮挡的物体,减少不必要的渲染计算。
Render Queue Sorting (渲染队列排序): 根据物体的渲染队列 (Render Queue) 对物体进行排序,确保正确的渲染顺序,例如先渲染不透明物体,再渲染透明物体。
Shadow Pass (阴影 Pass): 渲染阴影贴图,用于计算阴影效果。
Depth Pre-pass (深度预处理 Pass): 可选的 Pass,用于提前渲染场景的深度信息,可以用于优化某些渲染效果,例如屏幕空间反射。
Opaque Pass (不透明物体 Pass): 渲染场景中的不透明物体。
Post-processing Pass (后处理 Pass): 应用各种后处理效果,例如颜色校正、Bloom、景深等。我们自定义的 Color Adjust Post Process Pass 就属于这个阶段。
Transparent Pass (透明物体 Pass): 渲染场景中的透明物体。
Output to Screen (输出到屏幕): 将最终渲染结果输出到屏幕。
1.3 SRP 的优势:
高度可定制性: 开发者可以完全控制渲染流程,根据项目需求定制渲染管线,实现各种独特的渲染效果。
跨平台一致性: SRP 可以更好地保证项目在不同平台上的渲染效果一致性。
性能优化: 通过自定义渲染管线,开发者可以针对特定平台和项目需求进行性能优化。
模块化设计: SRP 采用模块化设计,易于扩展和维护。
2. Visual Scripting (可视化脚本)
Visual Scripting (可视化脚本) 是 Unity 引擎推出的一种无需编写代码即可创建游戏逻辑的方式。它使用节点图 (Node Graph) 的形式,将复杂的代码逻辑可视化,开发者可以通过拖拽节点、连接节点来创建游戏行为和交互。Visual Scripting 降低了游戏开发的门槛,使得非程序员也能参与到游戏逻辑的开发中,同时也提高了程序员的开发效率,可以快速原型设计和迭代。
2.1 代码实践:使用 Visual Scripting 创建简单的物体移动
以下示例展示了如何使用 Visual Scripting 创建一个简单的物体移动逻辑,使物体在按下空格键时向上跳跃。
安装 Visual Scripting 包: 在 Unity Package Manager 中搜索 "Visual Scripting" 并安装。
创建 Visual Scripting Graph: 在 Project 窗口中右键点击,选择 "Create" -> "Visual Scripting" -> "Script Graph",命名为 "ObjectMovementGraph"。
创建 Flow Machine 组件: 在场景中选择一个 GameObject,例如 Cube,然后添加 "Flow Machine" 组件。将 "ObjectMovementGraph" 拖拽到 Flow Machine 组件的 Graph 属性中。
编辑 Visual Scripting Graph: 双击 "ObjectMovementGraph" 打开 Visual Scripting 编辑器。
添加 Input - Keyboard Input 节点: 右键点击空白区域,选择 "Add Node" -> "Input" -> "Keyboard Input",选择 "Space" 键。
添加 Branch 节点: 右键点击空白区域,选择 "Add Node" -> "Logic" -> "Branch"。将 "Keyboard Input" 节点的 "Bool" 输出连接到 "Branch" 节点的 "Condition" 输入。
添加 Transform - Translate 节点: 右键点击空白区域,选择 "Add Node" -> "Transform" -> "Translate"。将 "Branch" 节点的 "True" 输出连接到 "Translate" 节点的 "Control Input" 输入。设置 "Translate" 节点的 "Vector" 输入为 (0, 1, 0),表示向上移动。
连接 GameObject: 将 "Translate" 节点的 "Target" 输入连接到 Flow Machine 组件的 GameObject。可以直接拖拽场景中的 Cube GameObject 到 "Target" 输入上,或者在 Graph Inspector 中选择 "Self"。
2.2 Mermaid 图表:Visual Scripting 节点图
内容详解:
Keyboard Input (Space) 节点: 监听空格键的按下事件,当空格键被按下时,输出 True 值。
Branch 节点: 条件分支节点,根据输入的布尔值 (Condition) 决定执行哪个分支。当 Condition 为 True 时,执行 True 分支;当 Condition 为 False 时,执行 False 分支。
Translate (0, 1, 0) 节点: 平移物体的位置。设置 Vector 输入为 (0, 1, 0) 表示沿 Y 轴正方向移动,即向上移动。
GameObject: 表示要操作的 GameObject,这里是 Cube 物体。
2.3 Visual Scripting 的优势:
可视化编程: 通过节点图的方式可视化代码逻辑,降低了编程门槛,易于理解和维护。
快速原型设计: 可以快速搭建游戏逻辑原型,验证游戏机制和玩法。
非程序员友好: 使得美术、设计师等非程序员也能参与到游戏逻辑的开发中。
提高开发效率: 对于简单的逻辑,使用 Visual Scripting 可以比编写代码更快。
易于学习和上手: Visual Scripting 的学习曲线相对平缓,容易上手。
3. Addressable Asset System (可寻址资源系统)
Addressable Asset System (可寻址资源系统,简称 Addressables) 是 Unity 引擎用于管理和加载资源的新系统。它取代了传统的 Resources 文件夹和 AssetBundle,提供了一种更灵活、更高效的资源管理方案。Addressables 允许开发者通过 "地址" (Address) 来引用和加载资源,而无需关心资源的物理路径和打包方式。它可以用于管理各种类型的资源,例如场景、Prefab、纹理、音频、Shader 等。
3.1 代码实践:使用 Addressables 加载 Prefab
以下代码示例展示了如何使用 Addressables 加载一个 Prefab 资源,并实例化到场景中。
安装 Addressables 包: 在 Unity Package Manager 中搜索 "Addressables" 并安装。
配置 Addressables Groups: 打开 Addressables Groups 窗口 (Window -> Asset Management -> Addressables -> Groups)。创建一个新的 Group,例如 "Prefabs"。
标记 Prefab 为 Addressable: 在 Project 窗口中选择一个 Prefab 资源,例如 "MyPrefab",然后在 Inspector 面板中点击 "Addressable" 按钮,将其添加到 "Prefabs" Group 中。可以自定义资源的 Address,默认情况下 Address 为资源的名称。
编写加载代码:
using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; public class LoadPrefabAddressable : MonoBehaviour { public string prefabAddress = "MyPrefab"; // Addressables 中配置的 Prefab 地址 public Transform spawnPoint; async void Start() { AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>(prefabAddress); await handle.Task; // 等待资源加载完成 if (handle.Status == AsyncOperationStatus.Succeeded) { GameObject prefab = handle.Result; Instantiate(prefab, spawnPoint.position, spawnPoint.rotation); } else { Debug.LogError("Failed to load prefab: " + prefabAddress); } Addressables.Release(handle); // 释放资源句柄 } }
内容详解:
加载资源: Addressables.LoadAssetAsync<GameObject>(prefabAddress) 方法用于异步加载指定地址的资源。prefabAddress 是我们在 Addressables Groups 中配置的 Prefab 资源的地址,例如 "MyPrefab"。该方法返回一个 AsyncOperationHandle<GameObject> 类型的异步操作句柄。
等待加载完成: await handle.Task 用于等待异步资源加载操作完成。
检查加载状态: handle.Status == AsyncOperationStatus.Succeeded 用于检查资源是否加载成功。
获取加载结果: handle.Result 获取加载成功的资源,这里是 GameObject 类型的 Prefab。
实例化 Prefab: Instantiate(prefab, spawnPoint.position, spawnPoint.rotation) 将加载的 Prefab 实例化到场景中的指定位置。
释放资源句柄: Addressables.Release(handle) 用于释放资源句柄,当资源不再需要使用时,应该及时释放资源句柄,以便 Addressables 系统可以进行资源管理和内存优化。
3.2 Mermaid 图表:Addressables 工作流程
内容详解:
Assets (资源): 项目中的各种资源,例如 Prefab、纹理、音频等。
Addressable Groups (可寻址资源组): 用于组织和管理 Addressable 资源的逻辑分组。可以将相关的资源放在同一个 Group 中,方便管理和打包。
Catalog (地址目录): Addressables 系统维护一个 Catalog,用于存储资源地址和资源实际位置的映射关系。在运行时,通过 Catalog 来查找资源。
Build Pipeline (构建管线): Addressables 系统在构建时,会根据 Group 的配置,将 Addressable 资源打包成 Bundles (Asset Packs)。
Bundles (资源包): 打包后的资源包,包含 Addressable 资源。
Runtime Loading (运行时加载): 在运行时,通过资源的 Address 从 Bundles 中加载资源。
3.3 Addressables 的优势:
灵活的资源管理: 通过 Address 地址来引用资源,解耦了资源引用和资源物理路径,提高了资源管理的灵活性。
高效的资源加载: Addressables 提供了异步加载、资源缓存、依赖管理等机制,提高了资源加载效率和运行时性能。
减少内存占用: Addressables 可以根据需要加载和卸载资源,减少内存占用。
热更新支持: Addressables 可以方便地实现资源的热更新,无需重新构建整个应用程序。
简化 AssetBundle 管理: Addressables 封装了 AssetBundle 的复杂性,简化了 AssetBundle 的创建和管理。
4. 新输入系统 (New Input System)
Unity 新输入系统 (New Input System) 是对旧输入系统的一次全面升级。它提供了更强大、更灵活、更易用的输入管理方案。新输入系统支持各种输入设备,例如键盘、鼠标、手柄、触摸屏、VR 控制器等,并提供了统一的输入事件处理机制。它采用 Input Actions 的概念,将物理输入动作 (例如按下按键、移动鼠标) 与游戏逻辑动作 (例如跳跃、射击) 解耦,使得输入管理更加清晰和模块化。
4.1 代码实践:使用 Input Actions 控制角色移动
以下代码示例展示了如何使用 Input Actions 来控制角色的移动。
安装 Input System 包: 在 Unity Package Manager 中搜索 "Input System" 并安装。
创建 Input Actions 资源: 在 Project 窗口中右键点击,选择 "Create" -> "Input Actions",命名为 "PlayerInputActions"。
配置 Input Actions: 双击 "PlayerInputActions" 打开 Input Actions 编辑器。
创建 Action Map: 默认情况下会创建一个名为 "Player" 的 Action Map。
创建 Actions: 在 "Player" Action Map 中,创建两个 Action: "Move" 和 "Jump"。
Move Action: Action Type 选择 "Value",Control Type 选择 "Vector2"。添加两个 Binding:"/wasd" 和 "/leftStick"。
Jump Action: Action Type 选择 "Button",添加一个 Binding:"/space" 和 "/buttonSouth"。
保存 Input Actions 资源。
生成 C# 代码: 在 Input Actions 编辑器中,点击 "Generate C# Class",生成 C# 脚本 "PlayerInputActions.cs"。
编写控制代码:
using UnityEngine; using UnityEngine.InputSystem; public class PlayerController : MonoBehaviour { private PlayerInputActions playerInputActions; private Vector2 moveInput; public float moveSpeed = 5f; public float jumpForce = 10f; private Rigidbody rb; private bool isGrounded; void Awake() { playerInputActions = new PlayerInputActions(); rb = GetComponent<Rigidbody>(); } void OnEnable() { playerInputActions.Player.Enable(); playerInputActions.Player.Move.performed += OnMovePerformed; playerInputActions.Player.Move.canceled += OnMoveCanceled; playerInputActions.Player.Jump.performed += OnJumpPerformed; } void OnDisable() { playerInputActions.Player.Disable(); playerInputActions.Player.Move.performed -= OnMovePerformed; playerInputActions.Player.Move.canceled -= OnMoveCanceled; playerInputActions.Player.Jump.performed -= OnJumpPerformed; } void FixedUpdate() { Move(); } void OnMovePerformed(InputAction.CallbackContext context) { moveInput = context.ReadValue<Vector2>(); } void OnMoveCanceled(InputAction.CallbackContext context) { moveInput = Vector2.zero; } void OnJumpPerformed(InputAction.CallbackContext context) { if (isGrounded) { Jump(); } } void Move() { Vector3 movement = new Vector3(moveInput.x, 0f, moveInput.y) * moveSpeed * Time.fixedDeltaTime; rb.MovePosition(rb.position + movement); } void Jump() { rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse); isGrounded = false; } void OnCollisionEnter(Collision collision) { if (collision.gameObject.CompareTag("Ground")) { isGrounded = true; } } }
内容详解:
创建 Input Actions 资源: 我们创建了一个 "PlayerInputActions.inputactions" 资源,并在其中定义了 "Move" 和 "Jump" 两个 Action。 "Move" Action 绑定了键盘的 WASD 键和手柄的左摇杆,"Jump" Action 绑定了键盘的空格键和手柄的 South Button。
生成 C# 代码: 通过 Input Actions 编辑器生成了 "PlayerInputActions.cs" 脚本,这个脚本包含了访问 Input Actions 的 C# 代码。
PlayerController 脚本: PlayerController.cs 脚本负责处理角色控制逻辑。
playerInputActions = new PlayerInputActions(); 创建了 "PlayerInputActions" 类的实例。
playerInputActions.Player.Enable(); 启用了 "Player" Action Map。
事件订阅: 通过事件订阅的方式处理输入事件。
playerInputActions.Player.Move.performed += OnMovePerformed; 当 "Move" Action 被触发 (例如按下 WASD 键或移动左摇杆) 时,调用 OnMovePerformed 方法。
playerInputActions.Player.Move.canceled += OnMoveCanceled; 当 "Move" Action 被取消 (例如松开 WASD 键或停止移动左摇杆) 时,调用 OnMoveCanceled 方法。
playerInputActions.Player.Jump.performed += OnJumpPerformed; 当 "Jump" Action 被触发 (例如按下空格键或手柄 South Button) 时,调用 OnJumpPerformed 方法。
事件处理方法:
OnMovePerformed(InputAction.CallbackContext context) 方法从 context 中读取 Vector2 类型的输入值,赋值给 moveInput 变量。
OnMoveCanceled(InputAction.CallbackContext context) 方法将 moveInput 重置为 Vector2.zero。
OnJumpPerformed(InputAction.CallbackContext context) 方法在角色处于地面状态时调用 Jump() 方法。
Move() 方法: 根据 moveInput 值计算移动方向和距离,使用 Rigidbody.MovePosition() 方法移动角色。
Jump() 方法: 给角色施加向上的 Impulse 力,实现跳跃效果,并将 isGrounded 设置为 false。
OnCollisionEnter() 方法: 检测角色与地面碰撞,将 isGrounded 设置为 true。
4.2 Mermaid 图表:新输入系统架构