当前位置: 首页 > news >正文

【unity实战】用unity实现一个3D俯视角暗杀潜行恐怖类游戏,主要是实现视野范围可视化效果

最终效果

在这里插入图片描述

文章目录

  • 最终效果
  • 前言
  • 实战
    • 1、俯视角角色移动控制
    • 2、创建角色视野,并获取视野内的目标
    • 3、场景视图可视化辅助线
    • 4、游戏视图显示实际视野
    • 5、只渲染视野内的内容
      • 5.1 新增模板掩码着色器。
      • 5.2 新增模板对象着色器。
      • 5.3 将Stencil Object材质添加到墙壁、地面、可见目标上
      • 5.4 修改相机背景颜色为黑色
      • 5.5 给ViewMesh视野网格添加Stencil Mask材质
      • 5.6 为了让效果更好,我们可以调大视野范围和修改视野角度
      • 5.7 运行效果
    • 6、阴影跟随视角移动
    • 7、场景视图显示所有对象
  • 项目源码
  • 参考
  • 专栏推荐
  • 完结

前言

在潜行类游戏中,视野范围的可视化是塑造紧张氛围和策略玩法的核心机制之一。无论是《刺客信条》中的敌人警戒系统,还是《合金装备》的锥形视野检测,良好的视野可视化不仅能增强玩家的沉浸感,还能提供清晰的信息反馈,帮助玩家制定战术决策。

在本文中,我们将使用 Unity 实现一个 3D俯视角潜行恐怖游戏 的敌人视野系统,重点探讨如何:

  • 动态生成锥形视野范围(使用Mesh渲染)

  • 利用Stencil Buffer实现遮挡效果(如墙壁后的敌人不可见)

  • 优化性能(减少不必要的射线检测)

  • 结合光照与阴影增强恐怖氛围

我们将从基础实现开始,逐步优化,最终打造一个高效、可扩展的视野检测系统,适用于各种潜行、恐怖或战术类游戏。如果你想要学习如何在Unity中高效处理视野检测与可视化,那么本教程将为你提供完整的实现思路和代码解析。

注意:本项目里墙壁的生成,我使用的是之前的迷宫实战项目的方法,如果想了解可以查看:【unity实战】使用unity程序化随机生成3D迷宫,不过不看也没关系,因为两个功能完全是独立的。
在这里插入图片描述

实战

1、俯视角角色移动控制

如果对Character Controller不了解的,可以参考:【零基础入门unity游戏开发——unity3D篇】unity CharacterController 3D角色控制器最详细的使用介绍,并实现俯视角、第三人称角色控制(附项目源码)

using UnityEngine;public class PlayerMovement : MonoBehaviour
{CharacterController characterController;Camera mainCamera;float horizontal;float vertical;Vector3 direction;public float speed = 5f; // 玩家移动的速度void Start(){characterController = GetComponent<CharacterController>(); // 初始化角色控制器mainCamera = Camera.main;}void Update(){SetPlayerMove();SetPlayerRotation();}//处理角色移动void SetPlayerMove(){horizontal = Input.GetAxis("Horizontal");vertical = Input.GetAxis("Vertical");direction = new Vector3(horizontal, 0, vertical);characterController.SimpleMove(direction.normalized * speed);}// 处理角色旋转void SetPlayerRotation(){// 将鼠标屏幕坐标转换为世界坐标,设置z值为相机y位置Vector3 mousePos = mainCamera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x,Input.mousePosition.y,mainCamera.transform.position.y));// 保持角色原有y轴高度mousePos.y = transform.position.y;// 让当前物体朝向鼠标世界坐标transform.LookAt(mousePos);}
}

挂载脚本
在这里插入图片描述
运行效果
在这里插入图片描述

2、创建角色视野,并获取视野内的目标

新增FieldOfView 脚本

using UnityEngine;
using System.Collections;
using System.Collections.Generic;public class FieldOfView : MonoBehaviour {public float viewRadius; // 视野半径[Range(0,360)]public float viewAngle; // 视野角度(0-360度)public LayerMask targetMask; // 目标层级public LayerMask obstacleMask; // 障碍物层级public List<Transform> visibleTargets = new List<Transform>(); // 可见目标列表void Start() {// 开始协程,每隔一定时间查找目标StartCoroutine (nameof(FindTargetsWithDelay), .2f);}// 延迟查找目标的协程IEnumerator FindTargetsWithDelay(float delay) {while (true) {yield return new WaitForSeconds (delay); // 等待指定延迟FindVisibleTargets (); // 查找可见目标}}// 查找可见目标的方法void FindVisibleTargets() {visibleTargets.Clear (); // 清空可见目标列表// 获取视野半径内的所有目标碰撞体Collider[] targetsInViewRadius = Physics.OverlapSphere (transform.position, viewRadius, targetMask);for (int i = 0; i < targetsInViewRadius.Length; i++) {Transform target = targetsInViewRadius [i].transform; // 获取目标变换组件Vector3 dirToTarget = (target.position - transform.position).normalized; // 计算到目标的归一化方向// 检查目标是否在视野角度内if (Vector3.Angle (transform.forward, dirToTarget) < viewAngle / 2) {float dstToTarget = Vector3.Distance (transform.position, target.position); // 计算到目标的距离// 检查目标之间是否有障碍物遮挡if (!Physics.Raycast (transform.position, dirToTarget, dstToTarget, obstacleMask)) {visibleTargets.Add (target); // 如果没有遮挡,添加到可见列表}}}}
}

我们将遮挡墙壁层级设置为Wall,可见目标设置为Target层,至于场景怎么绘制就大家自由发挥了。

挂载脚本并配置参数
在这里插入图片描述

效果,角色只要靠近目标,就会在visibleTargets列表追加对应目标数据
在这里插入图片描述

3、场景视图可视化辅助线

目前我们看不到角色具体的检测范围,可以编写脚本Editor,在场景视图绘制出可视化辅助线

修改FieldOfView代码

// 根据角度获取方向向量
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal) {if (!angleIsGlobal) {angleInDegrees += transform.eulerAngles.y; // 如果不是全局角度,加上物体的y轴旋转}return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad),0,Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}

新增FieldOfViewEditor脚本

using UnityEngine;
using UnityEditor;[CustomEditor (typeof (FieldOfView))]
public class FieldOfViewEditor : Editor {void OnSceneGUI() {FieldOfView fow = (FieldOfView)target; // 获取目标FieldOfView组件Handles.color = Color.white; // 设置手柄颜色为白色// 绘制视野半径的圆弧Handles.DrawWireArc (fow.transform.position, Vector3.up, Vector3.forward, 360, fow.viewRadius);// 计算视野角度的左右边界方向Vector3 viewAngleA = fow.DirFromAngle (-fow.viewAngle / 2, false);Vector3 viewAngleB = fow.DirFromAngle (fow.viewAngle / 2, false);// 绘制视野角度的边界线Handles.DrawLine (fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);Handles.DrawLine (fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);Handles.color = Color.red; // 设置手柄颜色为红色// 绘制到所有可见目标的连线foreach (Transform visibleTarget in fow.visibleTargets) {Handles.DrawLine (fow.transform.position, visibleTarget.position);}}
}

效果
在这里插入图片描述
如果你只想显示扇形视野范围,可以使用下面的脚本

using UnityEngine;
using UnityEditor;// FieldOfView 组件的自定义编辑器
[CustomEditor(typeof(FieldOfView))]
public class FieldOfViewEditor : Editor
{// 在场景视图中绘制GUIvoid OnSceneGUI(){// 获取当前编辑的目标对象(FieldOfView组件)FieldOfView fow = (FieldOfView)target;// 设置绘制颜色为白色Handles.color = Color.white;// 计算视野的左右边界方向向量// viewAngleA: 视野左边界方向(负角度方向)Vector3 viewAngleA = fow.DirFromAngle(-fow.viewAngle / 2, false);// viewAngleB: 视野右边界方向(正角度方向)Vector3 viewAngleB = fow.DirFromAngle(fow.viewAngle / 2, false);// 绘制视野范围的扇形弧线// 参数说明:// - 中心点位置:fow.transform.position// - 法线方向:Vector3.up (Y轴向上)// - 起始方向:viewAngleA// - 扇形角度:fow.viewAngle// - 扇形半径:fow.viewRadiusHandles.DrawWireArc(fow.transform.position, Vector3.up, viewAngleA, fow.viewAngle, fow.viewRadius);// 绘制从中心点到左边界的线段Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleA * fow.viewRadius);// 绘制从中心点到右边界的线段Handles.DrawLine(fow.transform.position, fow.transform.position + viewAngleB * fow.viewRadius);// 注意:这里绘制的是视野范围的示意图形// 实际游戏中的视野检测逻辑应该在FieldOfView脚本中实现}
}

效果
在这里插入图片描述

4、游戏视图显示实际视野

修改FieldOfView

[Header("视野网格参数")]
public float meshResolution;      // 网格分辨率(每度有多少条射线)
public int edgeResolveIterations; // 边缘解析迭代次数(用于平滑边缘)
public float edgeDstThreshold;    // 边缘距离阈值(决定何时需要细分边缘)[Header("网格引用")]
public MeshFilter viewMeshFilter; // 用于显示视野的网格过滤器
Mesh viewMesh;                    // 视野网格实例void Start()
{// 初始化视野网格viewMesh = new Mesh();viewMesh.name = "View Mesh";viewMeshFilter.mesh = viewMesh;// 开始协程,每隔一定时间查找目标StartCoroutine(nameof(FindTargetsWithDelay), .2f);
}void LateUpdate()
{DrawFieldOfView(); // 在LateUpdate中绘制视野,确保在所有更新完成后执行
}// 绘制视野范围
void DrawFieldOfView()
{int stepCount = Mathf.RoundToInt(viewAngle * meshResolution); // 计算需要多少步(射线)float stepAngleSize = viewAngle / stepCount; // 每一步的角度大小List<Vector3> viewPoints = new List<Vector3>(); // 存储所有视野边界点ViewCastInfo oldViewCast = new ViewCastInfo();for (int i = 0; i <= stepCount; i++){// 计算当前角度(从左侧开始到右侧)float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i;ViewCastInfo newViewCast = ViewCast(angle); // 发射射线检测碰撞// 从第二步开始检查边缘if (i > 0){bool edgeDstThresholdExceeded = Mathf.Abs(oldViewCast.dst - newViewCast.dst) > edgeDstThreshold;// 如果命中状态改变或距离超过阈值,需要查找边缘if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDstThresholdExceeded)){EdgeInfo edge = FindEdge(oldViewCast, newViewCast); // 查找精确边缘if (edge.pointA != Vector3.zero){viewPoints.Add(edge.pointA);}if (edge.pointB != Vector3.zero){viewPoints.Add(edge.pointB);}}}viewPoints.Add(newViewCast.point); // 添加当前点oldViewCast = newViewCast; // 保存当前点供下次比较}// 创建网格int vertexCount = viewPoints.Count + 1; // 顶点数(所有边界点+中心点)Vector3[] vertices = new Vector3[vertexCount];int[] triangles = new int[(vertexCount - 2) * 3]; // 三角形数vertices[0] = Vector3.zero; // 第一个顶点是中心点(本地坐标)for (int i = 0; i < vertexCount - 1; i++){// 将世界坐标转换为本地坐标vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]);// 构建三角形(扇形)if (i < vertexCount - 2){triangles[i * 3] = 0;triangles[i * 3 + 1] = i + 1;triangles[i * 3 + 2] = i + 2;}}// 更新网格viewMesh.Clear();viewMesh.vertices = vertices;viewMesh.triangles = triangles;viewMesh.RecalculateNormals();
}// 查找两个ViewCast之间的精确边缘
EdgeInfo FindEdge(ViewCastInfo minViewCast, ViewCastInfo maxViewCast)
{float minAngle = minViewCast.angle;float maxAngle = maxViewCast.angle;Vector3 minPoint = Vector3.zero;Vector3 maxPoint = Vector3.zero;// 使用二分法迭代查找精确边缘for (int i = 0; i < edgeResolveIterations; i++){float angle = (minAngle + maxAngle) / 2;ViewCastInfo newViewCast = ViewCast(angle);bool edgeDstThresholdExceeded = Mathf.Abs(minViewCast.dst - newViewCast.dst) > edgeDstThreshold;if (newViewCast.hit == minViewCast.hit && !edgeDstThresholdExceeded){minAngle = angle;minPoint = newViewCast.point;}else{maxAngle = angle;maxPoint = newViewCast.point;}}return new EdgeInfo(minPoint, maxPoint);
}// 从指定角度发射射线检测
ViewCastInfo ViewCast(float globalAngle)
{Vector3 dir = DirFromAngle(globalAngle, true);RaycastHit hit;if (Physics.Raycast(transform.position, dir, out hit, viewRadius, obstacleMask)){// 如果命中障碍物,返回命中信息return new ViewCastInfo(true, hit.point, hit.distance, globalAngle);}else{// 如果没有命中,返回最大距离的点return new ViewCastInfo(false, transform.position + dir * viewRadius, viewRadius, globalAngle);}
}// 根据角度获取方向向量
public Vector3 DirFromAngle(float angleInDegrees, bool angleIsGlobal)
{if (!angleIsGlobal){angleInDegrees += transform.eulerAngles.y; // 如果不是全局角度,加上物体的y轴旋转}return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
}// 视野投射信息结构体
public struct ViewCastInfo {public bool hit;     // 是否命中障碍物public Vector3 point; // 命中点或最大距离点public float dst;     // 距离public float angle;   // 角度public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle) {hit = _hit;point = _point;dst = _dst;angle = _angle;}
}// 边缘信息结构体
public struct EdgeInfo {public Vector3 pointA; // 边缘点Apublic Vector3 pointB; // 边缘点Bpublic EdgeInfo(Vector3 _pointA, Vector3 _pointB) {pointA = _pointA;pointB = _pointB;}
}

在角色下新增显示视野空物体,添加Mesh Filter和Mesh Renderer组件即可,Mesh Filter默认不需要有任何网格内容
在这里插入图片描述
配置参数
在这里插入图片描述
效果
在这里插入图片描述

在这里插入图片描述

5、只渲染视野内的内容

5.1 新增模板掩码着色器。

这个着色器在渲染时不会显示任何可见内容,但会在模板缓冲区中标记它所覆盖的像素为1,为后续的Stencil Object创建可见区域。

Shader "Custom/Stencil Mask"
{Properties{_Color ("Color", Color) = (1,1,1,1)_MainTex ("Albedo (RGB)", 2D) = "white" {}_Smoothness ("Smoothness", Range(0,1)) = 0.5_Metallic ("Metallic", Range(0,1)) = 0.0}SubShader{Tags { "RenderType" = "Opaque""RenderPipeline" = "UniversalPipeline""Queue" = "Geometry-100"}LOD 200ColorMask 0ZWrite OffStencil{Ref 1Pass Replace}Pass{Name "StencilMask"Tags { "LightMode" = "SRPDefaultUnlit" }HLSLPROGRAM#pragma vertex vert#pragma fragment frag#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"struct Attributes{float4 positionOS   : POSITION;float2 uv           : TEXCOORD0;};struct Varyings{float2 uv           : TEXCOORD0;float4 positionHCS  : SV_POSITION;};CBUFFER_START(UnityPerMaterial)float4 _MainTex_ST;half4 _Color;half _Smoothness;half _Metallic;CBUFFER_ENDTEXTURE2D(_MainTex);SAMPLER(sampler_MainTex);Varyings vert(Attributes IN){Varyings OUT;OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);return OUT;}half4 frag(Varyings IN) : SV_Target{// We don't actually output any color (ColorMask 0)// But we still sample the texture to maintain the same discard behaviorhalf4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv) * _Color;return 0;}ENDHLSL}}FallBack "Universal Render Pipeline/Unlit"
}

5.2 新增模板对象着色器。

这个着色器只会渲染在模板缓冲区中值为1的区域(即被Stencil Mask标记过的区域),实现选择性渲染效果。

Shader "Custom/Stencil Object"
{Properties{_Color ("Color", Color) = (1,1,1,1)_MainTex ("Albedo (RGB)", 2D) = "white" {}_Smoothness ("Smoothness", Range(0,1)) = 0.5_Metallic ("Metallic", Range(0,1)) = 0.0}SubShader{Tags { "RenderType" = "Opaque""RenderPipeline" = "UniversalPipeline""Queue" = "Geometry"}LOD 300Stencil{Ref 1Comp Equal}Pass{Name "ForwardLit"Tags { "LightMode" = "UniversalForward" }HLSLPROGRAM#pragma vertex vert#pragma fragment frag#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"struct Attributes{float4 positionOS   : POSITION;float2 uv           : TEXCOORD0;float3 normalOS     : NORMAL;};struct Varyings{float2 uv           : TEXCOORD0;float4 positionHCS  : SV_POSITION;float3 normalWS    : TEXCOORD1;float3 positionWS  : TEXCOORD2;};CBUFFER_START(UnityPerMaterial)float4 _MainTex_ST;half4 _Color;half _Smoothness;half _Metallic;CBUFFER_ENDTEXTURE2D(_MainTex);SAMPLER(sampler_MainTex);Varyings vert(Attributes IN){Varyings OUT;OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);OUT.uv = TRANSFORM_TEX(IN.uv, _MainTex);OUT.normalWS = TransformObjectToWorldNormal(IN.normalOS);OUT.positionWS = TransformObjectToWorld(IN.positionOS.xyz);return OUT;}half4 frag(Varyings IN) : SV_Target{// Sample the texturehalf4 color = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, IN.uv) * _Color;// Lighting calculationsInputData lightingInput = (InputData)0;lightingInput.positionWS = IN.positionWS;lightingInput.normalWS = normalize(IN.normalWS);lightingInput.viewDirectionWS = GetWorldSpaceNormalizeViewDir(IN.positionWS);lightingInput.shadowCoord = TransformWorldToShadowCoord(IN.positionWS);SurfaceData surfaceInput = (SurfaceData)0;surfaceInput.albedo = color.rgb;surfaceInput.alpha = color.a;surfaceInput.metallic = _Metallic;surfaceInput.smoothness = _Smoothness;surfaceInput.occlusion = 1.0; // You can add occlusion texture if needed// Apply lightinghalf4 finalColor = UniversalFragmentPBR(lightingInput, surfaceInput);return finalColor;}ENDHLSL}}FallBack "Universal Render Pipeline/Lit"
}

5.3 将Stencil Object材质添加到墙壁、地面、可见目标上

我们可以根据Stencil Object创建几个不同颜色的材质,分别将材质附加到墙壁、地面、可见目标上,如果有需要还可以添加贴图
在这里插入图片描述
效果,所有物品默认都消失不见了
在这里插入图片描述

5.4 修改相机背景颜色为黑色

在这里插入图片描述
效果
在这里插入图片描述

5.5 给ViewMesh视野网格添加Stencil Mask材质

在这里插入图片描述

5.6 为了让效果更好,我们可以调大视野范围和修改视野角度

在这里插入图片描述

5.7 运行效果

在这里插入图片描述

6、阴影跟随视角移动

修改FieldOfView

public float maskCutawayDst = 0.15f; //阴影摆动幅度//...vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i])  + Vector3.forward * maskCutawayDst;//...

效果
在这里插入图片描述

7、场景视图显示所有对象

目前如果不运行游戏,场景视图不会显示任何东西,这非常不利于我们调试和搭建场景。

我们可以新增一个Plane,将Stencil Mask材质配置给它,并将它的范围扩大,覆盖整个区域,并设置y轴比相机高。
在这里插入图片描述
这样我们在场景视图就可以看到物品了,而不影响游戏视图。

项目源码

https://gitee.com/xiangyuphp/unity-urpfield-of-view-game
在这里插入图片描述

参考

https://www.youtube.com/watch?v=xkcCWqifT9M&list=PLFt_AvWsXl0dohbtVgHDNmgZV_UY7xZv7&index=3


专栏推荐

地址
【unity游戏开发入门到精通——C#篇】
【unity游戏开发入门到精通——unity通用篇】
【unity游戏开发入门到精通——unity3D篇】
【unity游戏开发入门到精通——unity2D篇】
【unity实战】
【制作100个Unity游戏】
【推荐100个unity插件】
【实现100个unity特效】
【unity框架/工具集开发】
【unity游戏开发——模型篇】
【unity游戏开发——InputSystem】
【unity游戏开发——Animator动画】
【unity游戏开发——UGUI】
【unity游戏开发——联网篇】
【unity游戏开发——优化篇】
【unity游戏开发——shader篇】
【unity游戏开发——编辑器扩展】
【unity游戏开发——热更新】
【unity游戏开发——网络】

完结

好了,我是向宇,博客地址:https://xiangyu.blog.csdn.net,如果学习过程中遇到任何问题,也欢迎你评论私信找我。

赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!
在这里插入图片描述

http://www.lryc.cn/news/611498.html

相关文章:

  • X86-ubuntu22.04远程桌面只有1/4无法正常操作
  • 问题定位排查手记1 | 从Windows端快速检查连接状态
  • 分布式文件系统07-小文件系统的请求异步化高并发性能优化
  • ubuntu 22.04 中安装python3.11 和 3.11 的 pip
  • STM32U5 外部中断不响应问题分析
  • Ubuntu设置
  • DevOps时代的知识基座革命:Gitee Wiki如何重构研发协作范式
  • 基于51单片机的温控风扇Protues仿真设计
  • 【面试场景题】电商秒杀系统的库存管理设计实战
  • Python高级排序技术:非原生可比对象的自定义排序策略详解
  • 17.10 智谱AI GLM 篇:ChatGLM3-6B 快速上手
  • LeetCode每日一题,8-6
  • List、ArrayList 与顺序表
  • 软考软件设计师考点总结
  • 模电知识点总结
  • 安卓雷电模拟器安装frida调试
  • mysql优化策略
  • 【Excel】通过Index函数向下拖动单元格并【重复引用/循环引用】数据源
  • WinForm之ListView 组件
  • Ethereum: L1 与 L2 的安全纽带, Rollups 技术下的协作与区别全解析
  • Vue计算属性详解2
  • 无法解析 CentOS 官方镜像源的域名
  • 微软的BitLocker加密
  • 输电线路防外破声光预警装置 | 防山火/防钓鱼/防施工安全警示系统
  • 豆包新模型与PromptPilot工具深度测评:AI应用开发的全流程突破
  • UE编辑器相机窗口运行时相机fov 大小不一致
  • 嵌入式学习的第四十四天-ARM
  • 安装 cuda 版本 PyTorch(2025)
  • 【计算机网络】王道考研笔记整理(3)数据链路层
  • Python 通过Playwright+OpenCV破解滑动验证码 实例