Unity狙击手3D源码解析:弹道、狙击镜与AI实现
2026/9/15 15:04:25 网站建设 项目流程

简介:面向 Unity 移动端射击游戏开发者的完整 3D 狙击手 FPS 工程源码,使用 C# 编写,支持 Unity 2018.4.35f1 及以上版本。项目以反恐狙击为核心玩法,内置 20 个设计好的关卡,覆盖近距离遭遇战与中远距离狙击场景,包含不同敌人类型与地形障碍,操作流畅,并集成 AdMob 广告,可直接面向 iOS 与 Android 发布。资源包共 2000 个文件,大小约 958.6MB;除大量 meta 元数据与 cache 索引外,主体包括 179 个 C# 脚本(玩家控制、敌人 AI、枪械射击与换弹逻辑)、166 个材质、138 个 PNG 贴图、80 个 FBX 与 72 个 OBJ 模型、67 个 Shader、45 个 Prefab 预制体,以及动画、音频、PDF 文档等,目录结构清晰。工程中还包含 DOTween 插件脚本、枪械换弹/开火动画和沙漠场景动作资源,方便研究 FPS 战斗反馈、移动端优化和广告接入方式。当前已有 71 人学习下载,适合需要快速上手并发布移动 FPS 项目的初中级开发者参考,也便于进行二次开发。

1. 拿到 Sniper Army 3D 源码后的第一件事:场景与脚本分层

Sniper Army 3D(狙击大军3D)是一个把“狙击手射击”整个玩法打包成 Unity C# 源码的典型工程——场景、瞄准镜、弹道、敌人 AI、命中反馈都能在 Assets 目录里直接改。很多第一次打开这种项目的人会先进场景点 Play,结果不是枪不响就是瞄准镜黑屏;原因通常不是某个脚本写错了,而是没搞清这组源码的依赖顺序。先理清三件事:场景里挂着哪些相机与 UI 层、预制体和 ScriptableObject 配置是否绑定、入口脚本是挂在玩家身上还是挂在武器身上。把这三层理顺,后面的调参数、做二次开发就不会每改一处都连锁报错。

下面顺着这个思路,按“射击核心 → 狙击镜渲染 → AI 关卡 → 手感验证”四个环节拆解。适合手里已经拿了一套狙击手 3D 源码、正在对照着改需求的人,也适合还没开始、打算自己用 C# 在 Unity 里做一个狙击关卡的开发者。所有代码是通用实现思路,不是某个特定项目的逐行还原。

2. Sniper Army 3D 狙击核心:弹道、命中与伤害的 C# 参数设计

2.1 用 ScriptableObject 管理狙击枪械参数,别把数值写死在 MonoBehaviour 里

狙击游戏的武器参数非常多:伤害、弹速、射程、穿甲能力、后坐力、开镜时间。如果全部写死在 MonoBehaviour 里,玩家换枪就得复制脚本,后期调整平衡性时还要在一个几百行的类里翻找字段。常见做法是把一把枪定义成一个 ScriptableObject,用数据驱动的方式挂到武器与子弹上,这也是源码工程里最值得先看的文件。

using UnityEngine; [CreateAssetMenu(fileName = "SniperWeapon", menuName = "Sniper/WeaponConfig")] public class SniperWeaponConfig : ScriptableObject { public string weaponName; public float damage = 120f; public float range = 300f; public float bulletVelocity = 200f; public float spreadAngle = 0.02f; public int magazineSize = 5; public float reloadTime = 2.4f; public float recoilUp = 15f; public bool hasScope = true; }

在 Project 面板右键执行 Create → Sniper → WeaponConfig,生成资产后直接拖到武器控制器上即可。这样数值调整不用重新编译 C# 代码,运行中改完资产再进 Play Mode 就会生效。需要关注的参数是:bulletVelocity越大,远距离子弹下坠越小,但受物理步长影响,要配合弹头脚本里的子步进检测;spreadAngle是子弹实际飞行的锥形散布角,狙击枪应控制在 0.01~0.03 弧度;damage不建议设成固定值,最好作为基础值参与距离衰减计算。

2.2 从枪口到目标的弹头仿真:Raycast 单帧检测与子步进飞行

狙击射击有两种常见命中实现。第一种是单帧Physics.Raycast:从枪口朝准星方向发射一条射线,命中点即伤害点,表现干脆、逻辑最简单。第二种是“实体弹头”:生成一个轻量 GameObject,每帧让它前进一段距离,并对这段路径做射线检测,能支持抛物线下坠和移动目标命中。狙击手 3D 项目一般选第二种,原因是狙击镜里的高倍观察需要能看到落点,也让远距离射击的“提前量”成为玩法的一部分。

using UnityEngine; public interface IHittable { void OnHit(float damage, Vector3 point, Vector3 dir); } public class BulletProjectile : MonoBehaviour { public float velocity = 200f; public LayerMask hitMask; private void FixedUpdate() { float step = velocity * Time.fixedDeltaTime; if (Physics.Raycast(transform.position, transform.forward, out RaycastHit hit, step, hitMask)) { transform.position = hit.point; IHittable hittable = hit.collider.GetComponentInParent<IHittable>(); if (hittable != null) hittable.OnHit(120f, hit.point, transform.forward); Destroy(gameObject); return; } transform.position += transform.forward * step; } }

这段代码的关键是每物理帧只沿着射线移动velocity * Time.fixedDeltaTime的距离,并只检测该距离内是否有碰撞,避免子弹高速穿过薄墙出现隧穿。这里的hitMask要单独建立 Player、Enemy、Obstacle 三层,狙击枪最怕子弹被玩家自己的准星模型挡住。若把velocity调大到 500 以上,建议同时检查 Project Settings 里的Physics.autoSyncTransforms,否则多人联机时不同客户端看到的命中点会不一致。

2.3 伤害模型:爆头倍率、距离衰减与命中反馈的 C# 组织方式

伤害不能只写成一句HP -= value。狙击枪源码里值得单独建两个文件:IHittable接口和DamageInfo结构体。头部命中乘 2.5 倍、四肢减伤 30%、距离超过射程的 80% 后开始伤害递减,这些都要在伤害计算里集中处理。表格列一组我常用的默认值,方便对照调整。

部位基础伤害倍率典型弹种
头部2.5x所有狙击弹
胸部1.0x高伤穿甲弹
四肢0.7x普通弹
未判定部位0.2x流弹

狙击枪因为射速低,每一次开火的正反馈都必须非常强。命中标记、镜头轻微停顿、子弹落点特效、敌人的受击后仰动画,这些反馈不能散落在子弹脚本里。合理做法是让IHittable.OnHit内部统一触发一个HitMarkerManager,由它负责 UI 与特效播放。判断命中部位时,可以用headCollidertorsoCollider等子碰撞体的 tag 区分,也可以给每个敌人挂一个BodyPartHitbox组件,把倍率直接配在 Inspector 上,敌人模板越多越省事。这个设计还有一个额外好处:联机时只需要同步伤害事件而不是同步子弹坐标。

3. Sniper Army 3D 的狙击镜渲染:RenderTexture、阴影问题与 WebGL 存档

3.1 双相机狙击镜:RenderTexture 挂到 RawImage,而不是直接切 FOV

狙击手游戏的瞄准镜有两种实现:一种是直接改主相机 FOV,把镜头拉近,这种做法最快但镜框、十字线、镜内画质都很难做真实;另一种是双相机加 RenderTexture,狙击镜相机输出到一张纹理,再贴到 UI 的 RawImage 上。第二种是源码项目里最常见的做法,因为镜片畸变、渐变黑圈、镀膜反光都可以在 UI 层叠加。

using UnityEngine; using UnityEngine.UI; public class ScopeCameraController : MonoBehaviour { public Camera mainCamera; public Camera scopeCamera; public RawImage scopeImage; public RenderTexture scopeTexture; private void Awake() { scopeCamera.targetTexture = scopeTexture; scopeImage.texture = scopeTexture; } private void LateUpdate() { scopeCamera.transform.SetPositionAndRotation( mainCamera.transform.position, mainCamera.transform.rotation ); } }

关键点是LateUpdate同步,保证主相机旋转结束后,狙击镜相机再对齐,不会出现镜内画面滞后。RenderTexture 的分辨率不必一味求高,1024×1024 在狙击镜全屏时已经够用。狙击镜开启时,记得把主相机画面继续渲染在底层,这样镜外场景的跑动、天空盒都能保留。

RenderTexture 参数推荐值出问题时的表现
分辨率1024×1024低端设备开镜模糊
Depth Buffer24 bit镜内远处物体闪烁
antiAliasing2x树枝边缘锯齿明显
相机 cullingMaskEnemy + Scenery + GroundRawImage 黑屏或缺少物体

如果开镜后 RawImage 黑屏,先检查 scopeCamera 的cullingMask是否包含主相机所在的 UI 层;如果镜内物件穿透遮挡,再把 scopeCamera 的nearClipPlane调大到 0.3,避免枪械模型顶到镜头上。

3.2 狙击镜里的 Unity 阴影问题:阴影抖动与镜内剔除距离

狙击镜里最容易看到两类阴影问题:远处敌人脚下阴影抖动,近处地形阴影撕裂。抖动通常是Shadow Distance设得太大,阴影相机在远距离下深度精度不够。不要只调 Quality Settings 里的阴影距离,狙击镜相机和主相机应该使用不同的剔除距离。

LayerMask scopeMask = LayerMask.GetMask("Scenery", "Enemy", "Ground"); scopeCamera.cullingMask = scopeMask; scopeCamera.layerCullDistances[LayerMask.NameToLayer("Enemy")] = 300f; scopeCamera.layerCullDistances[LayerMask.NameToLayer("Scenery")] = 800f; scopeCamera.layerCullDistances[LayerMask.NameToLayer("Ground")] = 500f; scopeCamera.ResetCullingMatrix();

把主相机Shadow Distance收在 150~200 米,单独给 scopeCamera 设置更远的剔除距离,这样瞄准镜里能看到敌人,但阴影计算仍集中在主相机身边,性能与观感都能保住。地面阴影撕裂多半是地形高度图与阴影贴图分辨率不匹配,可以给地形单独指定低分辨率阴影或关闭地表接收阴影,只保留敌人投影。狙击镜里的准星需要用单独的 UGUI Image 放在 RawImage 顶层,否则准星会被场景阴影盖住。

3.3 WebGL 发布与 IDBFS 写入失败:狙击存档的桥接方案

部分狙击源码打包 WebGL 后,玩家打了几关,刷新页面进度丢失,控制台报IDBFS相关错误。这是 Unity WebGL 文件系统映射到 IndexedDB 时的常见问题,Application.persistentDataPath在浏览器无痕模式或存储配额不足时无法写入。正确处理是先检测文件系统可用性,再做本地内存兜底。

public static void SaveSnapshot(string json) { #if UNITY_WEBGL PlayerPrefs.SetString("Sniper_Save", json); PlayerPrefs.Save(); #else string path = Path.Combine(Application.persistentDataPath, "sniper_save.json"); File.WriteAllText(path, json); #endif }

这段代码只是解决同步写入异常,并不能保证浏览器不清理 IndexedDB。更稳的方案是提供一个IFileBridge接口,WebGL 侧允许玩家手动导出.sav文件,加载时通过 Unity 的TextAsset或浏览器 File API 读回。需要注意,Unity WebGL 下的Input.mousePosition受 CSS 缩放影响,开镜后屏幕中心点与子弹命中点容易偏移,建议固定使用Screen.width / 2f并转成 UI 坐标再判断是否在镜内。

注意:PlayerPrefs 在 WebGL 里同样依赖 IndexedDB,无痕模式照样可能失效;狙击玩法进度条较长时,首选手动存档导出。

4. Sniper Army 3D 的敌人 AI 与关卡推进:状态机、NavMesh 与事件驱动

4.1 敌人状态机最小骨架:巡逻、警戒、战斗的切换条件

狙击手 3D 游戏的可玩性一半在枪,另一半在敌人行为。源码里最常见的是状态机型 AI:巡逻、警戒、搜索、攻击。行为树更灵活,但交付型源码项目用 C# 状态机更好改,所有状态可见,且能在 Inspector 里直接调整阈值。

public enum EnemyState { Patrol, Investigate, Combat } public class SniperEnemyAI : MonoBehaviour { public EnemyState state = EnemyState.Patrol; public Transform player; public float viewDistance = 80f; public float viewAngle = 60f; public LayerMask obstacleMask; private void Update() { bool canSeePlayer = DetectPlayer(); switch (state) { case EnemyState.Patrol: if (canSeePlayer) state = EnemyState.Combat; break; case EnemyState.Combat: if (!canSeePlayer) state = EnemyState.Investigate; break; } } private bool DetectPlayer() { Vector3 toPlayer = player.position + Vector3.up - transform.position; if (Vector3.Angle(transform.forward, toPlayer.normalized) > viewAngle) return false; if (toPlayer.magnitude > viewDistance) return false; return !Physics.Raycast(transform.position, toPlayer.normalized, out _, toPlayer.magnitude, obstacleMask); } }

这段代码最值得调的是viewDistanceviewAngle。狙击关卡讲的是“你先发现他,还是他先发现你”,所以敌人视野不能太强。我一般把巡逻状态下的viewDistance设置为玩家狙击枪有效射程的 60%,viewAngle取 50~70 度。比设置数值更重要的是状态切走的时机:敌人从 Combat 转 Investigate 后,应该记住最后看见玩家的位置,而不是立刻恢复巡逻,否则玩家一趴下敌人就失去目标。

4.2 掩体寻路与 3D 遮挡判断:用 NavMeshAgent 实现受击找掩体

敌人中弹后要会找掩体,否则狙击手只是在打活靶。最常见的工程解是 Unity 的 NavMeshAgent:先烘焙场景静态碰撞体的 NavMesh,再在敌人生成时缓存几个掩体点位。受击后调用SetDestination走到安全点,而不是随机跑。

private Vector3 FindBestCover(Vector3 threatPos, Vector3[] coverPoints) { Vector3 selfPos = transform.position; Vector3 awayDir = (selfPos - threatPos).normalized; float bestScore = -1f; Vector3 best = selfPos; foreach (Vector3 point in coverPoints) { Vector3 toCover = point - selfPos; float dot = Vector3.Dot(toCover.normalized, awayDir); if (dot < 0f) continue; float distanceCost = toCover.magnitude / 100f; float score = dot - distanceCost; if (score > bestScore) { bestScore = score; best = point; } } return best; }

这段函数的评分逻辑是:掩体点相对玩家方向的反向重合度越高越好,同时距离越近越好,防止敌人为了一个掩体跑半个地图。NavMeshAgentradius要设小一点,狙击掩体场景里掩体之间的窄通道很多,默认 0.5 的半径会导致敌人卡墙;设成 0.1 并开启AutoTraverseOffMeshLink更稳。还要注意,受击找掩体不要每帧调用,应该在受击回调中延迟 0.2 秒执行,多个敌人同时受击时错开寻路计算。

AI 参数常见默认值影响
viewDistance80m发现玩家的距离
viewAngle60°视野锥形角度
arriveDistance1.5m掩体停留点精度
rotSpeed8°/s转身速度,狙击靶子重要手感

4.3 事件驱动关卡:从击杀到下一波流程,别把敌人脚本写成关卡脚本

“狙击大军”意味着同一场景里有多波次敌人。源码里建议用 UnityEvent 做事件总线,而不是把波次判断写在每个敌人脚本里。一个简单的 WaveTrigger 可以监听敌人死亡事件,并在数量归零后激活下一波。

using UnityEngine; using UnityEngine.Events; public class WaveTrigger : MonoBehaviour { public UnityEvent onWaveCompleted; public void RegisterEnemy(SniperEnemyAI enemy) { enemy.onDied.AddListener(CheckWaveState); } private void CheckWaveState() { if (FindObjectsOfType<SniperEnemyAI>().Length == 0) onWaveCompleted?.Invoke(); } }

注意FindObjectsOfType不能在 Update 中每帧调用,它会产生明显的 GC 压力,WebGL 下还会让 IDBFS 写入卡顿更明显。正确的做法是每个敌人在OnDestroy时通知 WaveTrigger,然后用一个计数器递减。关卡触发器、任务目标、救援人质、定时进攻都应该由关卡管理器订阅,而不是让敌人脚本知道当前处于第几波。这样后续新增敌人类型或者修改波次顺序时,不需要动 AI 代码。

5. Sniper Army 3D 手感验证:命中线、滑动条与按钮热区

5.1 用 Debug.DrawLine 把子弹命中线画出来

狙击枪开完一枪看不到命中点,新手第一反应是调伤害。正确做法是先验证子弹是否确实飞到了准星位置。可以用Debug.DrawLine或 LineRenderer 把枪口到命中点的轨迹画出来,至少保留 3 秒,方便在 Scene 视图里回看。

using UnityEngine; public class ScopeDebugDrawer : MonoBehaviour { public Transform muzzle; public LineRenderer line; public void DrawShot(Vector3 target) { Debug.DrawLine(muzzle.position, target, Color.yellow, 3f); line.positionCount = 2; line.SetPosition(0, muzzle.position); line.SetPosition(1, target); } }

BulletProjectile命中回调中调用DrawShot(hit.point),就能立刻看出子弹落点与准星的偏差。这个技巧在调狙击镜同步、弹道下坠和散布参数时非常有用。

5.2 用滑动条调灵敏度,用父节点扩大按钮点击范围

狙击镜甩枪的灵敏度需要一个可调滑块。用 UGUI 的 Slider 绑定鼠标灵敏度,注意把 0~1 的滑块值映射到实际旋转速度范围。

public Slider sensitivitySlider; public MouseLook mouseLook; private void Start() { sensitivitySlider.onValueChanged.AddListener(v => { float mapped = Mathf.Lerp(0.5f, 3f, v); mouseLook.xSensitivity = mapped; mouseLook.ySensitivity = mapped; }); }

这里的Mathf.Lerp是为了让滑块低端也有可用灵敏度,避免滑到最小时视角完全转不动。另一个和手感直接相关的细节是“开镜按钮”的点击热区。Unity UGUI 的按钮是否可点取决于Image.raycastTarget的矩形范围,所以想扩大点击范围,不要去改按钮本身的缩放,而是在按钮上层放一个透明 Image 并勾选raycastTarget。从 uGUI 源码的角度看,这样点击事件会先落在透明 Image 上,再通过事件系统冒泡到子按钮,玩家就算按偏一点也能正常开关狙击镜。验证方式是在 Scene 视图打开 UI 调试,确认热区已经覆盖住目标区域。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询