ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用Unity从零搭建地下室恐怖Demo:光照、AI与跳跃惊吓的完整实现

用Unity从零搭建地下室恐怖Demo:光照、AI与跳跃惊吓的完整实现 做恐怖题材独立游戏时最先遇到的难题往往不是怪物建模而是场景里明明摆了桌子、椅子、管道和血迹玩家走进去却一点都不害怕。这个问题的根源在于恐怖感不是靠贴图堆出来的而是靠“技术组合”逼出来的。光照、音效、玩家操作受限程度、敌人行为节奏四者缺一氛围就会瞬间塌掉。本文以《The Joy of Creation - The Basement》作为主题灵感用 Unity 从零搭建一个可玩的地下室恐怖 Demo。你会实现第一人称视角控制、手电筒电量系统、闪烁灯光、脚步声、环境音效、怪物 AI 追击和跳跃惊吓触发器。所有代码均给出完整 C# 示例场景只用 Unity 内置图元也能跑通适合新手入门也适合想快速验证玩法的开发者直接复用。1. 背景与核心概念1.1 为什么是“地下室”地下室是恐怖作品里出现频率最高的舞台之一。从空间心理学角度看它具备三个天然优势封闭性空间没有开阔的逃生方向身后永远是未知区域玩家的注意力会被持续往背后拉扯。低照度昏暗环境让视野信息变得残缺玩家必须依赖手电筒和声音判断周围恐惧来自“看不清”。杂物密度高箱子、管道、门缝、通风口天然适合做视线遮挡、掩体追逐和触发点不需要额外设计复杂地形。所以在做恐怖 Demo 时与其一开始就规划一整个大场景不如先用一间 10×10 的地下室把核心玩法跑通。场景越小灯光和音效对氛围的影响越容易被感知。1.2 恐怖氛围的核心技术要素一个恐怖场景的氛围由四类技术要素构成要素作用典型实现手段灯光控制玩家视野范围与安全感点光源、聚光灯、闪烁、暗角、雾效音频在画面出现之前先建立心理预期环境底噪、3D 定位、定向触发音效玩家控制制造受限感和紧张感移动速度限制、手电筒电量、脚步声反馈敌人 AI 与触发器把压力具象化为可交互对象状态机、NavMesh 追击、跳跃惊吓触发这四个要素不是独立工作的。比如闪烁灯光会在玩家面前制造短暂的“视野消失”环境音效填补这个空白怪物 AI 趁这个窗口拉近距离一次 Jump Scare 的惊吓值才会被完全释放。做恐怖游戏本质上就是设计这套“节奏”。1.3 技术选型Unity 3D 方案概览本文选择 Unity 而不是其他引擎主要考虑三点实时灯光成熟Spot Light、Point Light 可以直接在场景里摆放搭配自写脚本就能实现闪烁、频闪效果。AI 导航内置NavMesh 烘焙加一个 NavMeshAgent 组件就能让敌人在迷宫般的地下室里自动寻路追击玩家。开发迭代快C# 脚本通过 Inspector 暴露参数后策划和开发可以在 Play 模式里实时调整灯光强度、移动速度、跳跃惊吓时长。下文所有示例基于 Unity 内置渲染管线Built-in Render Pipeline不依赖具体 Unity 小版本如果你用 URP 或 HDRP灯光参数单位会略有不同需要按实际管线微调。2. 环境准备与项目配置2.1 Unity 版本与项目模板版本建议使用 Unity 2021.3 LTS 或 2022.3 LTS这两个版本都是长期支持版本稳定性更好。打开 Unity Hub选择3D Core模板创建项目。需要注意的是Unity 每个大版本之间AI 导航菜单、光照窗口的位置可能发生变化。例如老版本的 Navigation 窗口在Window AI Navigation而新版 Unity 需要先通过 Package Manager 安装AI Navigation包再使用NavMeshSurface组件。具体以你本机的 Unity 版本菜单为准本文会同时说明两种方式的思路。2.2 项目目录结构在动手写代码前先建好目录。一个清晰的目录结构能让你在后续加系统、换美术资源时不必到处乱翻。Assets/ ├── Scenes/ │ └── Basement.unity ├── Scripts/ │ ├── Player/ │ │ ├── FirstPersonController.cs │ │ ├── FlashlightController.cs │ │ ├── FootstepController.cs │ │ └── PlayerHealth.cs │ ├── Enemy/ │ │ └── BasementMonsterAI.cs │ └── Environment/ │ ├── FlickeringLight.cs │ └── AmbientAudioController.cs ├── Models/ ├── Materials/ ├── Audio/ └── Prefabs/Models、Materials、Audio、Prefabs一开始可以是空目录但提前规划好能避免后期资源文件散落整个 Assets 文件夹。2.3 基础场景搭建地下室的骨架完全用 Unity 内置图元搭建即可零资源依赖方便先跑通玩法再做美术替换。按以下步骤操作新建场景命名为Basement保存到Assets/Scenes。创建一个Plane命名为FloorScale 设为(10, 1, 10)作为地下室地面。创建四个Cube拉伸成墙壁每面墙的 Scale 大约是(10, 3, 0.2)围绕地板四周摆放留一面的中间位置作为门口。门口两侧用两个高度为 3 的Cube组成门框中间留出约 1.5 个单位宽的通道。用若干Cube、Cylinder、Sphere模拟箱子、管道、桌椅和杂物。摆放时故意留出一些“视线死角”方便后面测试敌人 AI 和惊吓触发器。为什么先这样搭因为内置图元不依赖网络下载和版权资源场景的碰撞体自动生成玩家不会穿模同时物体数量少运行帧率不受影响。等玩法验证通过后再把这些占位物体替换成正式的 3D 模型Prefab 的引用关系不会变。3. 玩家控制第一人称控制器与交互3.1 第一人称视角控制第一人称控制建议使用CharacterController而不是Rigidbody。原因是恐怖游戏通常不需要物理弹跳和受力反馈CharacterController 的胶囊碰撞体在走斜坡、过门槛时更稳定也不会因为物理引擎的微小抖动导致角色抖动。下面这个脚本同时处理移动和视角旋转。注意视角旋转的拆分水平旋转施加给玩家物体垂直旋转施加给摄像机子物体。如果直接把上下俯仰旋转施加给玩家胶囊体会让胶囊体倾斜碰撞体形状错乱角色会卡进墙角。// 文件路径Assets/Scripts/Player/FirstPersonController.cs using UnityEngine; public class FirstPersonController : MonoBehaviour { [Header(移动参数)] public float walkSpeed 3.5f; public float runSpeed 5.5f; public float lookSensitivity 2f; [Header(视角限制)] public float maxPitch 80f; [Header(摄像机引用)] public Transform cameraTransform; private CharacterController controller; private float pitch 0f; private float verticalVelocity 0f; private void Awake() { controller GetComponentCharacterController(); Cursor.lockState CursorLockMode.Locked; Cursor.visible false; } private void Update() { // 水平旋转施加给玩家物体 float mouseX Input.GetAxis(Mouse X) * lookSensitivity; float mouseY Input.GetAxis(Mouse Y) * lookSensitivity; // 垂直旋转只施加给摄像机防止胶囊体倾斜 pitch Mathf.Clamp(pitch - mouseY, -maxPitch, maxPitch); cameraTransform.localRotation Quaternion.Euler(pitch, 0f, 0f); transform.Rotate(Vector3.up * mouseX); // WASD 移动 float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 move transform.right * h transform.forward * v; if (move.magnitude 1f) { move.Normalize(); } float speed Input.GetKey(KeyCode.LeftShift) ? runSpeed : walkSpeed; // 简易重力 if (controller.isGrounded) { verticalVelocity -2f; } else { verticalVelocity Physics.gravity.y * Time.deltaTime; } Vector3 velocity move * speed Vector3.up * verticalVelocity; controller.Move(velocity * Time.deltaTime); } }场景里的组装方式创建一个空物体Player挂上CharacterController和FirstPersonController。把场景原有的Main Camera拖到Player下Local Position设为(0, 1.6, 0)表示人眼高度。将cameraTransform拖到脚本的对应槽位。这里的三个参数建议直接拖到 Inspector 里调walkSpeed控制在走廊里慢速潜行的节奏runSpeed不要设太高因为逃跑时速度过快会让玩家失去对环境的感知恐怖感会下降。3.2 手电筒与电池手电筒是恐怖场景里最经典的道具。它同时承担两个功能一是给玩家提供有限的视野二是制造“能源焦虑”。没有电量限制的手电筒只是个普通灯而限制电量的手电筒会让玩家在黑暗中反复思考“要不要打开”。给相机的子物体Camera上创建一个子物体Flashlight挂一个Spot Light组件朝向摄像机前方。脚本如下// 文件路径Assets/Scripts/Player/FlashlightController.cs using UnityEngine; public class FlashlightController : MonoBehaviour { public Light flashlight; public KeyCode toggleKey KeyCode.F; public float maxBattery 100f; public float batteryDrainSpeed 5f; public float lowBatteryThreshold 20f; private float battery; private float smoothIntensity; private bool isOn; private void Start() { battery maxBattery; smoothIntensity flashlight.intensity; flashlight.enabled false; } private void Update() { if (Input.GetKeyDown(toggleKey)) { isOn !isOn; flashlight.enabled isOn; } if (isOn) { battery - batteryDrainSpeed * Time.deltaTime; if (battery 0f) { battery 0f; isOn false; flashlight.enabled false; return; } } else { battery Mathf.Min(maxBattery, battery batteryDrainSpeed * 0.5f * Time.deltaTime); } // 低电量时光源强度轻微抖动模拟电源不稳 float flicker battery lowBatteryThreshold ? Mathf.Sin(Time.time * 12f) * 0.15f : 0f; smoothIntensity Mathf.Lerp(smoothIntensity, 3f flicker, 5f * Time.deltaTime); flashlight.intensity smoothIntensity; } }这段脚本的关键点是手电筒默认关闭玩家必须主动按下F键打开。默认关闭会让玩家进入场景的第一秒就开始尝试摸索操作紧张感提前建立。电量在开启时持续消耗关闭时缓慢恢复。这个设计比“只能靠电池道具恢复”更灵活玩家需要在探索和续航之间做取舍。低电量时用Mathf.Sin制造强度微抖模拟灯丝不稳。这个细节很小但玩家在安静的地下室里会非常敏锐地察觉到光源变化。3.3 脚步声与移动反馈脚步声是很多新手容易遗漏的部分。没有脚步声玩家会觉得自己像个幽灵在漂移沉浸感立刻下降。反向思考如果开始移动时有清晰的脚步声玩家就会下意识压低操作速度安静的环境会反过来放大玩家的谨慎心理。// 文件路径Assets/Scripts/Player/FootstepController.cs using UnityEngine; public class FootstepController : MonoBehaviour { public AudioSource footstepSource; public AudioClip[] concreteSteps; public float stepInterval 0.45f; private CharacterController controller; private float timer; private void Awake() { controller GetComponentCharacterController(); } private void Update() { Vector3 velocity controller.velocity; bool moving controller.isGrounded velocity.magnitude 1f; if (moving) { timer Time.deltaTime; if (timer stepInterval) { timer 0f; if (concreteSteps.Length 0) { AudioClip clip concreteSteps[Random.Range(0, concreteSteps.Length)]; footstepSource.PlayOneShot(clip); } } } else { timer 0f; } } }把数组concreteSteps拖入 3 到 5 段不同音调的水泥地脚步声每次播放时随机选一段避免脚步声变成机械重复。stepInterval可以配合主角当前的移动状态做动态修正越紧张、移动越慢时脚步声间隔反而要适当拉长让玩家有“每一步都踩在自己心上”的感觉。4. 灯光系统构建黑暗氛围4.1 全局光照与雾效地下室的黑暗感不能只靠“把所有灯关掉”那样只会得到一片死黑什么都看不见恐怖感反而不强。正确做法是降低环境默认亮度同时保留少量局部光源。在 Unity 中依次设置打开Window Rendering Lighting的 Environment 面板。将Skybox Material从默认天空盒换成纯黑色材质或者直接设为 None。恐怖场景不需要蓝天白云天空盒的光照信息会破坏黑暗感。把Ambient Mode设置为FlatAmbient Intensity调到0.05左右。这样场景里没有被光源照射的区域只会呈现极低亮度的灰色而不是纯黑。开启雾效。在Lighting面板的 Other Settings 中勾选Fog颜色取深蓝黑色例如#0B0E14Density 设为0.04到0.08。雾效的作用非常重要它能“吃掉”场景远端的细节让玩家只能看到一个模糊轮廓。在恐怖游戏里“看不清远处”比“看不见远处”更能制造压迫感因为模糊轮廓会诱导玩家脑补出更多可怕的东西。4.2 闪烁灯光脚本地下室里的场景灯应该是阴森的、不稳定的。新建一个脚本FlickeringLight.cs挂在任意场景光源上它会让灯光在随机区间内改变强度并偶尔完全熄灭一段时间。// 文件路径Assets/Scripts/Environment/FlickeringLight.cs using UnityEngine; public class FlickeringLight : MonoBehaviour { [Header(灯光引用)] public Light targetLight; [Header(闪烁参数)] [Range(0f, 1f)] public float minIntensity 0f; public float maxIntensity 1f; public float changeIntervalMin 0.05f; public float changeIntervalMax 0.3f; [Range(0f, 1f)] public float blackoutChance 0.15f; private float timer; private float nextChange; private void Awake() { if (targetLight null) { targetLight GetComponentLight(); } } private void Start() { nextChange Random.Range(changeIntervalMin, changeIntervalMax); targetLight.intensity minIntensity; } private void Update() { timer Time.deltaTime; if (timer nextChange) return; timer 0f; nextChange Random.Range(changeIntervalMin, changeIntervalMax); if (Random.value blackoutChance) { targetLight.intensity 0f; } else { targetLight.intensity Random.Range(minIntensity, maxIntensity); } } }参数里最值得研究的是blackoutChance它表示灯光闪烁过程中完全熄灭的概率。建议设成 0.1 到 0.2不要太高。完全熄灭是恐怖游戏里非常强的信号玩家会立刻意识到“接下来要发生什么”用太频繁反而失效。4.3 阴影与光源参数地下室灯光布置建议分三层主灯泡场景中央偏上位置放一个 Spot Light模拟天花板吊灯Range 8 到 12Spot Angle 60 到 90Intensity 3 到 5阴影选 Soft Shadows。局部光源在门缝、管道缝隙、角落放几个 Point Light强度低一些用偏冷色或偏绿的光色制造“来历不明”的发光源。玩家手电筒挂在相机下的 Spot LightRange 15 左右Angle 40 到 50Shadow Type 选 Hard Shadows这样物体边缘会出现清晰的锐利阴影视觉对比更强。关于阴影有一个重要的性能认知每个开启实时阴影的灯光都会增加绘制开销。在移动端或低端 PC 上实时阴影灯光的数量尽量控制在 2 到 3 个以内推荐把实时阴影留给玩家手电筒和场景主灯其他光源全部关闭阴影。5. 音频系统声音比画面更早吓到你5.1 环境低鸣与循环音效环境音效是恐怖场景的底层底色它的作用不是“吓人”而是让玩家意识到这个空间不是静止的。低频低鸣可以让人的心跳和呼吸不自觉地受影响这是生理层面的反应不需要玩家理解发生了什么。// 文件路径Assets/Scripts/Environment/AmbientAudioController.cs using UnityEngine; public class AmbientAudioController : MonoBehaviour { public AudioSource ambientSource; public AudioClip ambientClip; public float maxVolume 0.8f; public float fadeSpeed 0.5f; private void Start() { if (ambientSource null) { ambientSource gameObject.AddComponentAudioSource(); } ambientSource.clip ambientClip; ambientSource.loop true; ambientSource.spatialBlend 0f; // 2D 背景音 ambientSource.volume 0f; ambientSource.Play(); } private void Update() { ambientSource.volume Mathf.MoveTowards(ambientSource.volume, maxVolume, fadeSpeed * Time.deltaTime); } }背景音效开始播放时音量从 0 缓慢过渡到目标值而不是瞬间出现。突然出现的大音量会让玩家意识到“被音频接管了”从而产生出戏感缓慢填入的背景音则像空气一样自然渗透。5.2 3D 定位音效水管滴水、远处门缝刮擦声、金属管壁回音这些都应该用 3D 空间音效实现。人耳能通过音量衰减和时间差判断音源方向这是恐怖游戏中“听到身后有东西”的核心技术原理。对一个 AudioSource 做如下设置AudioSource source gameObject.AddComponentAudioSource(); source.spatialBlend 1f; // 完全 3D 化 source.minDistance 1f; // 最近距离小于该距离音量不再增加 source.maxDistance 20f; // 最远可听距离 source.dopplerLevel 0.3f; // 轻微多普勒效应 source.playOnAwake false;spatialBlend 1表示声音完全受距离影响minDistance和maxDistance之间的关系决定了音量的衰减曲线。在地下车环境里maxDistance可以适当调小因为封闭空间的声学效果比开阔场地衰减更快。3D 音效的使用原则是“只放关键音”。不要把所有杂物都配上声音否则空间会听起来像一个混乱的集市。选择 2 到 3 个固定声源比如角落的水管滴漏、通风口的风噪声、走廊尽头的金属摩擦就已经足够建立诡异的空间感。5.3 惊吓音效触发时机惊吓音效Sting是整个音频系统里最需要克制的部分。它的触发时机比音量更重要。经验准则是先有声音后有画面在怪物出现在视野之前 0.2 到 0.5 秒播放即时音效让玩家先“听到不对”再“看到不对”惊吓效果更强。短促高频使用 0.5 秒以内的短促高频音效不要用长持续音长声音会让玩家产生“这只是音效”的判断。控制频次一个 10 分钟左右的 Demo 里强烈惊吓不超过 3 次。每次惊吓之间的间隔需要足够长让玩家重新回到手电筒微光营造的安全感里。6. 敌人 AI 与惊吓机制6.1 AI 状态机敌人 AI 不用一开始就写复杂的感知系统用一个状态机就够了。三种状态Idle待机、Chase追击、Attack攻击。状态之间通过玩家距离切换。// 文件路径Assets/Scripts/Enemy/BasementMonsterAI.cs using UnityEngine; public class BasementMonsterAI : MonoBehaviour { public enum State { Idle, Chase, Attack } public State currentState State.Idle; public Transform player; public float chaseSpeed 2.5f; public float attackDistance 1.2f; public float detectionRange 8f; public float loseRange 15f; private UnityEngine.AI.NavMeshAgent agent; private void Awake() { agent GetComponentUnityEngine.AI.NavMeshAgent(); } private void Update() { if (player null) return; float distance Vector3.Distance(transform.position, player.position); switch (currentState) { case State.Idle: if (distance detectionRange) { currentState State.Chase; } break; case State.Chase: agent.isStopped false; agent.speed chaseSpeed; agent.SetDestination(player.position); if (distance attackDistance) { currentState State.Attack; } else if (distance loseRange) { currentState State.Idle; } break; case State.Attack: agent.isStopped true; player.GetComponentPlayerHealth()?.TakeDamage(100); currentState State.Idle; break; } } }注意这里没有使用using UnityEngine.AI;而是直接使用完全限定名UnityEngine.AI.NavMeshAgent。在混合使用多个命名空间时这种写法能避免类名冲突也让读者一眼看出这个组件的来源。6.2 检测与追击逻辑这段 AI 的追击原理是每帧计算敌方与玩家的直线距离。这个方案简单直观适合新手理解。它也有局限当场景里存在大量墙壁和遮挡物时“直线距离触发”会让敌人在隔着一堵墙的情况下也进入追踪状态。进阶方案可以增加一个“视线检测”条件用Physics.Raycast从怪物头部向玩家方向发射射线如果射线被墙壁挡住则不进入追击状态。NavMesh 的烘焙是敌人能自动寻路的必要条件Unity 2021 及更早版本打开Window AI Navigation窗口把地板和地面物体勾选到合适的 Navigation Area 层级点击Bake。Unity 2022 及更新版本需要先通过 Package Manager 安装AI Navigation包然后给地面物体添加NavMeshSurface组件点击Bake。烘焙后在 Scene 视图里可以看到地面上一层蓝色网格那就是敌人可以行走的区域。如果你发现怪物在追击时卡在墙角不动优先检查这块区域的烘焙结果。6.3 跳跃惊吓触发器Jump Scare 的触发方式一般是在某个门口、转角或箱子后面放置一个 Trigger 碰撞体玩家进入后触发器被激活。这里的核心是激活逻辑只能执行一次否则玩家反复踩进同一区域会反复触发体验会变成喜剧。// 文件路径Assets/Scripts/Enemy/JumpscareTrigger.cs using UnityEngine; using UnityEngine.SceneManagement; public class JumpscareTrigger : MonoBehaviour { public GameObject monster; public Transform scarePosition; public AudioClip scareSound; public float scareDuration 1.5f; private bool triggered false; private void OnTriggerEnter(Collider other) { if (triggered || !other.CompareTag(Player)) return; triggered true; // 把怪物瞬间移动到玩家面前并切换到面向玩家 monster.transform.position scarePosition.position; monster.transform.rotation scarePosition.rotation; monster.SetActive(true); if (scareSound ! null) { AudioSource.PlayClipAtPoint(scareSound, transform.position); } Invoke(nameof(RestartLevel), scareDuration); } private void RestartLevel() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } }这个脚本在触发后直接重载关卡效果等同于“玩家被抓住后游戏重开”不需要额外处理怪物动画。如果你不希望死亡就重开可以把RestartLevel改成调用 UI 面板显示 Game Over 后再让玩家选择重试。配合上面的 AI玩家还需要一个PlayerHealth组件否则BasementMonsterAI里调用的TakeDamage方法不存在会因为空引用而报错// 文件路径Assets/Scripts/Player/PlayerHealth.cs using UnityEngine; using UnityEngine.SceneManagement; public class PlayerHealth : MonoBehaviour { public int maxHealth 100; [HideInInspector] public int currentHealth; private void Start() { currentHealth maxHealth; } public void TakeDamage(int amount) { currentHealth - amount; if (currentHealth 0) { Die(); } } private void Die() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } }把PlayerHealth挂到Player物体上然后把Player拖进BasementMonsterAI的player槽位同时把Monster的 Tag 设置为Player确保触发器能通过CompareTag(Player)识别玩家。7. 完整运行与验证7.1 场景对象层级完成所有组件挂载后Hierarchy 窗口的层级结构应类似这样Basement ├── Environment │ ├── Floor │ ├── Walls │ │ ├── Wall_Front_Left │ │ ├── Wall_Front_Right │ │ ├── Wall_Back │ │ └── ... │ ├── Props │ │ ├── Box_01 │ │ ├── Box_02 │ │ ├── Pipe_01 │ │ └── ... │ └── Lighting │ ├── CeilingLight (Light FlickeringLight) │ └── CornerLight (Light FlickeringLight) ├── Player │ ├── CharacterController │ ├── Camera (localPosition 0, 1.6, 0) │ │ └── Flashlight (Spot Light) │ └── AudioListener ├── Monster (NavMeshAgent BasementMonsterAI) └── EventSystem检查每盏灯的targetLight是否拖到了对应的Light组件检查Monster的player引用是否指向Player。这两个是最常见的漏配错误。7.2 进入 Play 模式验证点击 Play 按钮按下面的清单逐项验证鼠标移动时视角是否顺滑上下视角是否被限制在 80 度内。按下F手电筒是否正常开关电量是否逐秒下降。在场景中走动是否有脚步声音量是否合适。观察头顶灯光是否随机闪烁是否偶尔熄灭再恢复。主动走向怪物距离小于detectionRange后怪物是否朝玩家移动。走向 JumpScare 触发器怪物是否被瞬间传送到面前音效是否播放1.5 秒后是否重开场景。如果以上 6 项全部通过这个地下室恐怖 Demo 的核心循环已经成立玩家在黑暗里探索灯光闪烁制造不安怪物追逐制造压力跳杀完成一次高潮。7.3 打包发布验证没问题后可以把游戏打包成独立可执行文件。打开File Build Settings点击Add Open Scenes把当前场景加入 Build 列表选择 Windows 平台点击Build。在Project Settings Player中填写Company Name和Product Name并设置一个应用图标。打包时如果选择Development Build可以看到更多运行日志方便在测试电脑上排查问题正式发布时则取消该选项以获得更接近最终用户的性能表现。8. 常见问题与排查思路8.1 角色穿墙、卡墙或者直接坠落出现这类问题优先检查两处CharacterController是否挂在玩家根物体上角色碰撞体中心是否在合理位置。胶囊体如果设置太高头顶会突出墙壁导致玩家看起来“卡在天花板里”。墙壁、地板是否有Mesh Collider或Box Collider。用内置 Cube 搭建的墙体自带Box Collider但如果你替换成美术模型别忘了重新调整碰撞体。另外如果角色移动太快CharacterController.Move会遇到“穿墙”问题。解决办法是把updatePosition改为更小步长或者降低runSpeed让每帧位移不超过墙体厚度。8.2 场景全黑或者过曝全黑和全白都不是恐怖氛围属于光照参数错误。全黑通常是Ambient Intensity设成了 0且场景里没有放置任何光源全白则通常是默认天空盒还在起作用并且开了多盏高强度灯光。排查顺序先确认Lighting面板里 Ambient Intensity 是否在 0.03 到 0.1 之间然后只保留一盏灯光调节 Intensity 观察效果最后再逐盏增加判断哪盏灯是过曝的元凶。8.3 敌人不移动、不追击怪物完全不动的情况95% 是这些原因问题现象常见原因解决思路怪物始终 Idleplayer引用未拖入或detectionRange太小检查 Inspector把 Player 拖入引用调大检测距离怪物原地抽搐NavMesh 未烘焙打开 Navigation/NavMeshSurface 重新 Bake怪物追到一半停下地面被标记为 Not Walkable检查地面物体的 Navigation Area 层级怪物追击后报错玩家没挂PlayerHealth给 Player 添加PlayerHealth组件8.4 卡顿与性能问题恐怖场景因为大量使用实时灯光和雾效很容易出现帧率波动。如果你发现进入灯光密集区域后掉帧明显优先做两件事减少实时阴影灯光数量。在三盏以上实时阴影同时开启的场景里开销几乎是成倍增长的。把不动的箱子、墙壁设为静态物体Static然后使用光照烘焙。受光物体烘焙后不再消耗实时阴影计算资源。音频侧的卡顿往往是“每一帧都在创建 AudioSource”。避免在 Update 中频繁PlayClipAtPoint正确做法是预先创建固定数量的 AudioSource 池播放时只切换 Clip。9. 最佳实践与工程建议9.1 场景与资源管理Prefab 化灯光、怪物、跳跃触发器、可交互杂物都应该做成 Prefab。后续调试参数时只需修改一处所有实例同步更新。命名规范场景物体统一使用英文名称并带上类型前缀如Light_Ceiling、Prop_Pipe_01、Trigger_Jumpscare_01。命名混乱在场景物体超过几十个后会严重拖慢开发效率。素材版权音频和模型尽量使用有明确授权的资源。恐怖类 Demo 如果计划公开分享一定要检查音效来源的许可证避免版权风险。9.2 性能预算控制恐怖游戏效果和质量之间需要做平衡。建议给项目设定一份简单的性能预算实时灯光总数不超过 5 个其中开启实时阴影的不超过 2 个。同时播放的 AudioSource 不超过 15 个环境底噪、3D 音效、UI 音效分别归类到不同的 Audio Mixer 分组方便统一调节音量。场景内单帧 Draw Call 尽量控制在 200 以内。使用内置图元时把同材质物体合并到同一个 GameObject 下可以减少 Draw Call。9.3 代码设计建议所有关键参数通过[Header]、[Tooltip]、[Range]暴露到 Inspector。这样调试时可以直接在编辑器里拖动参数观察效果而不用反复改代码。怪物 AI 的状态切换使用enum不要堆一组bool变量。bool组合会在状态数量超过三个后变得无法维护。脚本里对可能为空的引用做判空检查比如if (targetLight null)。空引用异常是初学者最容易踩的坑防御式写法能省掉大量 Debug 时间。9.4 恐怖节奏设计技术只是工具恐怖体验的最终效果取决于节奏。关于节奏有几点经验值得记住前 30 秒不要有惊吓。玩家刚进入场景时先让他熟悉操作、听环境音、看灯光闪烁。压力需要时间积累。声音先于画面。怪物出现之前先让它制造声音。脚步声、低吼、金属刮擦都可以作为“即将出现”的预告。适当使用假触发。在危险的跳跃惊吓之前安排一个玩家接近后什么也不会发生的空房间。玩家的警惕会被消耗真正的跳杀效果才会翻倍。10. 总结与学习路线10.1 核心收获通过这个地下室恐怖 Demo你实际上掌握了一套完整的 Unity 恐怖场景搭建流程用内置图元快速搭建封闭场景通过光照、雾效、音频建立视觉与听觉的双重限制感。第一人称控制器实现了移动、视角分离和重力手电筒系统引入了电量焦虑。闪烁灯光脚本和环境音效控制器展示了如何用随机参数制造不稳定感。怪物 AI 状态机加上 NavMesh 寻路让敌人可以在地下室中追击玩家。跳跃惊吓触发器把所有技术要素串联成了一个可玩的完整关卡循环。这套流程不局限于地下室。过道、废弃医院、仓库甚至太空站都是同样的技术框架只需要更换场景元素和音效素材。10.2 下一步可以继续深入的方向如果你想让这个 Demo 变成更完整的作品可以优先研究这几个方向URP 后处理给项目接入 URP使用 Bloom、色差、暗角效果画面质感会立刻提升一档。物品交互系统给箱子、门、柜子增加交互提示玩家需要收集钥匙或电池才能推进探索。更聪明的敌人 AI加入视线检测、听觉检测、巡逻路径和攻击冷却让追逐不再只是“看到你就冲过来”。动态音频混音使用 Unity Audio Mixer 做低频滤波和动态压缩让环境音随玩家心率或危险等级实时变化。如果你能把本文里的 Demo 完整跑通再基于它做一个 5 到 10 分钟的完整关卡你会对“游戏氛围到底是怎么被技术造出来的”这件事拥有一个非常具体的答案。建议先不要急着加更多怪物而是把灯光、音效和触发器节奏再各调两轮直到你自己在测试时都会下意识握紧鼠标这说明你的地下室里已经长出真正的恐怖感了。
返回列表