ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

太空银河飞机大战源码拆解:Unity射击游戏核心机制与实战技巧

2026/9/15 13:37:42 拓冰建站 浏览量
太空银河飞机大战源码拆解:Unity射击游戏核心机制与实战技巧 简介这是一份基于Unity 5.6及以上版本的太空银河飞机大战完整项目源码采用C#编写适合有基础Unity操作经验、希望系统学习2D射击游戏从游戏框架、战斗系统到商业化落地的开发者。包内共2000个文件、282.71MB核心包括261个C#脚本、多个Prefab预制体、Unity场景与Asset配置另有379张PNG美术贴图、JSON数据文件及JSLib交互库其中C#脚本承载战斗逻辑、界面控制与广告调用Prefab对应飞船、敌人和Boss预设PNG素材覆盖飞船、弹道与特效整体目录层级清晰方便按模块阅读与复用。游戏本身包含超过60个关卡、多种可升级飞船、Boss战、全球排行榜和幸运轮盘等系统还完整接入AdMob、Facebook Audience Network、Vungle广告SDK和IAP内购可作为研究移动端变现与支付模块的实例。目前已有168人学习浏览既能帮助新人了解Unity项目整体结构也能为中高级开发者提供跨平台发布与广告集成参考。1. 太空银河飞机大战源码值得拆开的理由「Space Squadron - Galaxy Shooter」这类 Unity 太空射击源码项目零散分布在资源商店和代码托管平台上。很多人 clone 下来跑通就关掉觉得「也就那样」。但它真正的价值在于这个体量的工程刚好覆盖了完整游戏循环玩家输入、子弹发射、敌机生成、碰撞计分、血量 UI、状态切换全链路只有几千行代码。无论是做完教程想接触真实工程结构的新手还是想验证休闲射击玩法的中级开发者这类源码都是性价比极高的阅读对象。下面按实战视角把这种工程从结构到运行机制完整拆一遍附上能直接复用的参数设计和常见误用的坑。2. 从 Galaxy Shooter 源码反推玩法与模块划分2.1 场景入口与 GameManager 的职责拿到一套 Unity 游戏源码第一件事不是看文件夹名而是在 Scene 层级里找挂脚本的空物体。太空射击游戏源码最常见的划分是 Player、Enemy、UI、Manager 四类目录入口通常是场景里的 GameManager 或 GameController 单例。这类单例一般放在一个空物体上挂DontDestroyOnLoad专门负责状态切换和跨场景数据保存。public enum GameState { Menu, Playing, Paused, GameOver } public class GameManager : MonoBehaviour { public static GameManager Instance { get; private set; } public GameState State { get; private set; } void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); } public void StartGame() { State GameState.Playing; ScoreManager.Instance.ResetScore(); PlayerHealth.Instance.ResetHealth(); WaveSpawner.Instance.StartWaves(); } }逻辑说明Instance是全局访问点整个工程里任何脚本都能通过GameManager.Instance拿到当前状态。DontDestroyOnLoad保证切场景时游戏状态不重置这是街机式游戏的标准做法。参数说明Destroy(gameObject)那行用来处理场景里重复挂 Singleton 的情况属于防御性代码没有也不影响短流程 demo但做项目最好保留。阅读时只需要跟住 State 的跳转Menu → Playing → GameOver就能知道整体生命周期。一旦某帧出现「敌人不动了」的现象优先查是不是State没进 Playing而不是先去翻敌人 AI。2.2 Player 脚本的移动边界与射击间隔太空飞机大战里玩家的移动控制几乎都是Transform.Translate 边界 Clamp很少有人用物理引擎的 AddForce因为街机型游戏要求按键即时响应物理会带来拖泥带水的手感。边界值不在 Update 里重复计算而是在 Start 里通过主相机换算一次保存成字段复用。public class PlayerShip : MonoBehaviour { [SerializeField] private float moveSpeed 8f; [SerializeField] private float fireInterval 0.12f; [SerializeField] private GameObject bulletPrefab; [SerializeField] private Transform firePoint; private float _left, _right, _top, _bottom; private float _lastFireTime; void Start() { Camera cam Camera.main; float halfHeight cam.orthographicSize; float halfWidth halfHeight * cam.aspect; _left -halfWidth 0.5f; _right halfWidth - 0.5f; _top halfHeight - 0.5f; _bottom -halfHeight 0.5f; } void Update() { float h Input.GetAxis(Horizontal); float v Input.GetAxis(Vertical); Vector3 delta new Vector3(h, v, 0) * (moveSpeed * Time.deltaTime); Vector3 pos transform.position delta; pos.x Mathf.Clamp(pos.x, _left, _right); pos.y Mathf.Clamp(pos.y, _bottom, _top); transform.position pos; if (Input.GetButton(Fire1) Time.time - _lastFireTime fireInterval) { _lastFireTime Time.time; SpawnBullet(); } } }逻辑说明Vector3 delta new Vector3(h, v, 0) * (moveSpeed * Time.deltaTime)保证移动速度与帧率无关这是所有角色控制脚本的基础。Mathf.Clamp把玩家限制在相机视野内0.5f 是给飞船模型留的边缘余量。参数说明这个冗余量取决于飞船图片的物理宽度如果模型较大或较小应该替换为sprite.bounds.extents动态读取否则会出现「看起来已经卡进边界」的差一帧问题。fireInterval是手感的关键0.08 到 0.15 秒是街机射击的合理区间。低于 0.05 秒时子弹数量会激增如果下方没有对象池老设备上会出现可感知的卡顿。2.3 对象池在源码中的角色与实现太空射击是一个高频生成和销毁对象的场景。子弹飞行几百毫秒就消失敌机批量生成又被击败如果用Instantiate Destroy每一发子弹都会产生一次内存分配和 GC 回收游戏运行几分钟后就会出现周期性的帧率毛刺。Unity 官方推荐的做法就是对象池预先创建一批实例用的时候激活用完禁用而不是销毁。下面是一份适合子弹和敌机共用的泛型对象池源码里常见的 PoolManager 变体本质也是这个结构public class ObjectPoolT where T : Component { private readonly StackT _available new StackT(); private readonly T _prefab; private readonly Transform _parent; public ObjectPool(T prefab, Transform parent, int preloadCount) { _prefab prefab; _parent parent; for (int i 0; i preloadCount; i) { T item Object.Instantiate(prefab, parent); item.gameObject.SetActive(false); _available.Push(item); } } public T Get() { T item _available.Count 0 ? _available.Pop() : Object.Instantiate(_prefab, _parent); item.gameObject.SetActive(true); return item; } public void Return(T item) { item.gameObject.SetActive(false); _available.Push(item); } }逻辑说明StackT的Pop/Push是 O(1) 操作比 List 的头部移除效率高所以在高频取放场景里优先用栈。preloadCount是预热数量直接影响游戏启动后的卡顿位置。参数说明子弹池预载 50 到 100 个即可覆盖屏幕上同时存在的子弹量敌机池预载当前波次最大数量的两倍比如一波最多出 15 架就预载 30 个。注意Return里没有检查重复入池。如果同一个物体被回收两次Stack里会存在重复引用下次Get会拿到一个已经激活的物体导致逻辑错乱。稳妥做法是在Return里先判断item.gameObject.activeSelf只有激活状态才执行入池逻辑。模块划分参考下表的典型目录结构读源码时可以照着这个映射快速定位模块职责典型目录核心类型常见代码量玩家控制Assets/Scripts/PlayerPlayerShip, PlayerHealth100 - 300 行敌人生成Assets/Scripts/EnemyEnemyBase, WaveSpawner200 - 400 行子弹与战斗Assets/Scripts/CombatBullet, ObjectPool150 - 350 行UI 表现Assets/Scripts/UIScoreManager, HealthBar100 - 250 行全局状态Assets/Scripts/ManagerGameManager, AudioManager80 - 200 行3. 子弹朝向、敌机波次与 UGUI 联动的实操细节3.1 三向子弹发射的朝向参数写法读源码时会发现射击方向的处理有两类一类直接用transform.up发射另一类先计算目标方向再发射。前者适用于固定朝向游戏后者适用于带瞄准机制的玩法。太空银河飞机大战这类传统竖版射击游戏正确的做法是前者加角度偏移。Galaxy Shooter 里常见的升级项是「火力1一次发三颗子弹」。实现它的核心不是加两个发射点而是复用同一个发射点旋转角度// 在 PlayerShip.SpawnBullet 中按火力等级扩展 float[] angles { -15f, 0f, 15f }; foreach (float angle in angles) { GameObject bullet bulletPool.Get(); bullet.transform.position firePoint.position; bullet.transform.rotation Quaternion.Euler(0, 0, angle); bullet.GetComponentBullet().Initialize(transform.up, bulletSpeed); }逻辑说明Quaternion.Euler(0, 0, angle)只在 Z 轴旋转因为 2D 精灵的默认正面是 Y 轴方向子弹脚本里用dir向量移动而不是读取transform.rotation这样方向和旋转解耦。参数说明-15f和15f是左右散开角度0f是正中子弹。这个数组可以直接替换成「两发时用 {-10, 10}五发时用 {-30, -15, 0, 15, 30}」做成配置数据后火力升级只需要改数组不用动发弹逻辑。这样做的另一个好处是Boss 的扇形弹幕和散射子弹完全复用同一套逻辑只是把角度数组换成循环生成的渐变角度避免为每种弹幕各写一个发射函数。3.2 敌机波次设计的两种数据驱动方式源码项目常见两种波次配置一种是把敌机列表直接暴露在 Inspector 的public Wave[] waves字段里另一种是用Resources.Load读 JSON 或 CSV。前者适合编辑器内调整后者适合策划改表。在 Unity 里做持久化且直观的配置我一般推荐 ScriptableObject因为它能在 Inspector 中引用预制体还不受场景保存的冗余字段干扰。[CreateAssetMenu(fileName WaveConfig, menuName Galaxy Shooter/Wave Config)] public class WaveConfig : ScriptableObject { public GameObject[] enemyPrefabs; public float spawnInterval 0.8f; public int enemyCount 10; public float moveSpeed 3f; public int scoreValue 100; }使用方式是在 Project 窗口右键创建多个 WaveConfig 资产分别配置不同波次的敌机种类、出生间隔和移动速度再把这些资产挂到 WaveSpawner 的数组里。代码结构上注意WaveSpawner 只依赖WaveConfig上这五个字段不关心具体预制体内容。参数说明spawnInterval在 0.5 到 1.2 秒之间波动时玩家压力感较好低于 0.3 秒会因同屏敌机数量暴增而拉高 DCDraw Call需要配合对象池控制瞬时生成量。获取波次数据时用Resources.LoadAllWaveConfig(Waves)可以批量加载但前提是资源放在 Resources 目录下。这个目录会打包所有资源进包体如果只是几张表完全可以接受。3.3 UGUI 血条、计分器与按钮点击范围扩展太空射击游戏的 HUD 通常是左上角分数、右上角血量条用的是 UGUI 的Slider组件。血条响应血量变化时不要每帧在 Update 里赋值而是让血条脚本注册一个刷新方法在伤害发生时调用。源码里常见的问题是把 Slider.value 放在 PlayerHealth 的 Update 里轮询白白消耗每帧 CPU。public class HealthBar : MonoBehaviour { [SerializeField] private Slider slider; public void SetHealth(float current, float max) { slider.maxValue max; slider.SetValueWithoutNotify(current / max); } }参数说明SetValueWithoutNotify是 Slider 的专用方法不触发onValueChanged回调。如果直接用slider.value ...且 Slider 上挂了监听会造成一次无意义的函数调用在高频伤害刷新时这种开销会被放大。同样逻辑适用于计分器只在ScoreManager.AddScore内部更新文本而不是每帧把整数转换成字符串。关于「Unity 如何扩大按钮的点击范围」这个场景在手机上小按钮的点击判定区域很难精确命中。常见做法是给按钮的 Image 设置alphaHitTestMinimumThreshold让透明像素不参与点击判定。但这样做首先要将图片纹理的 Read/Write 勾选开启会增加内存。更轻量级的做法是把透明点击层设计成独立子物体// 给按钮加一个透明子物体专门负责接收点击 public static class ButtonClickArea { public static void Expand(Button button, float expandRatio) { Image display button.GetComponentImage(); GameObject hitArea new GameObject(HitArea, typeof(RectTransform), typeof(Image)); hitArea.transform.SetParent(button.transform, false); hitArea.transform.SetAsFirstSibling(); RectTransform hitRect hitArea.GetComponentRectTransform(); hitRect.anchorMin Vector2.zero; hitRect.anchorMax Vector2.one; hitRect.offsetMin new Vector2(-expandRatio, -expandRatio); hitRect.offsetMax new Vector2(expandRatio, expandRatio); Image hitImage hitArea.GetComponentImage(); hitImage.color Color.clear; } }逻辑说明按钮的视觉图片放在显示层透明 HitArea 放在子物体第一位并且范围略大负责拦截射线。SetAsFirstSibling让透明层在渲染顺序上位于显示层之下最前面的子物体最后绘制因此不会盖住显示内容。参数说明expandRatio单位是像素按设备屏幕宽度动态计算会更合理比如Screen.width * 0.02f。4. 摄像机跟随、分辨率和 WebGL 存档部署的跨端排错4.1 摄像机跟随的平滑参数与边界限制太空银河飞机大战的视野跟随有两种模式摄像机锁定在玩家身上或者地图无限滚动、摄像机只在 Y 轴小范围移动。战场越开阔直接硬跟随越容易让画面抖动所以常用SmoothDamp做软跟随能滤除玩家自身快速的上下抖动public class CameraFollow : MonoBehaviour { [SerializeField] private Transform target; [SerializeField] private float smoothTime 0.08f; [SerializeField] private Vector3 offset new Vector3(0, 0, -10); private Vector3 _velocity; void LateUpdate() { Vector3 desired new Vector3(target.position.x, target.position.y, offset.z); transform.position Vector3.SmoothDamp(transform.position, desired, ref _velocity, smoothTime); } }逻辑说明LateUpdate的执行时机在 Update 之后所有角色移动完成后相机再定位避免相机比角色早一帧而出现角色在屏幕上的微小抖动。参数说明smoothTime是达到目标位置的大约时间0.05 到 0.1 秒手感偏紧0.2 秒偏软。若飞行游戏的摄像机在任何轴都不需要跟随直接把 desired 对应轴设为固定值即可比如竖版射击只在 Y 轴跟随、X 轴锁定在 0。4.2 Unity 阴影问题在 2D 场景里的处理方式2D 太空射击游戏一般不该开启动态实时阴影但很多源码项目默认从 3D 模板创建场景里带着一盏 Directional Light阴影设置还是 ShadowMode 的实时模式。实际运行中你会发现两个问题第一SpriteRenderer 的透明材质在实时阴影下产生撕裂的阴影投影第二动态阴影会打断 Unity 的合批流程阴影接收对象的 Draw Call 翻倍。处理方案是把光源的 Shadow Type 设为 No Shadows或者对场景中所有 SpriteRenderer 关闭阴影投射与接收。只有在飞船爆炸、陨石碎裂瞬间需要遮蔽感时用一张带模糊边缘的黑色半透明 Sprite 模拟假阴影即可。4.3 分辨率设置与刘海屏安全区适配Unity 的分辨率设置在Screen.SetResolution但在移动端和 WebGL 上后期改分辨率会带来画质和 UI 适配的双重复杂度。源码项目在 PC 上能正常跑发到手机或浏览器后按钮被刘海屏遮挡属于最常见的翻车场景。适配要对 Canvas 同时做两件事一是 CanvasScaler 选择 Scale With Screen Size参考分辨率设为 1920x1080二是代码适配 SafeArea。public class SafeAreaFitter : MonoBehaviour { [SerializeField] private RectTransform panel; void Start() { Rect safe Screen.safeArea; Vector2 min safe.position; Vector2 max safe.position safe.size; min.x / Screen.width; min.y / Screen.height; max.x / Screen.width; max.y / Screen.height; panel.anchorMin min; panel.anchorMax max; } }逻辑说明Screen.safeArea返回的是物理像素坐标必须除以屏幕宽高转成 0 到 1 的锚点值才能交给anchorMin / anchorMax。参数说明这个脚本放在 HUD 根节点上让整个面板收拢到安全区。如果做成通用组件可以监听OnRectTransformDimensionsChange以应对运行时设备旋转不过太空射击类游戏通常锁定竖屏或横屏Start 执行一次即可。4.4 Unity 发布 WebGL 后 IDBFS 写入失败的处理热词「Unity 发布 WebGL 使用 IDBFS 写入失败」指向的是 PlayerPrefs 在浏览器环境下的持久化机制。WebGL 构建里 PlayerPrefs 基于 Emscripten 的 IDBFS 文件系统写入 IndexedDB确切的失败场景有浏览器隐私模式禁用了 IndexedDB、Safari 的 ITP 限制、存储配额满等。源码项目如果带最高分存档功能WebGL 版常在这里报写入失败。处理思路是捕获异常并对玩家做降级提示而不是让崩溃阻断游戏public static void SaveHighScore(int score) { PlayerPrefs.SetInt(HighScore, score); try { PlayerPrefs.Save(); } catch (System.Exception ex) { Debug.LogWarning($存档失败降级为内存模式: {ex.Message}); _memoryFallback score; } }逻辑说明PlayerPrefs.Save()在 WebGL 平台是同步刷盘操作失败时以异常形式抛出。加 try/catch 后至少保证游戏流程不断掉。参数说明_memoryFallback是静态字段只在当前页面生命周期内有效刷新页面最高分会丢失所以需要在 UI 上提示用户「浏览器环境未开启持久化存储」。如果项目需要更稳定的存档可以改为把数据序列化后交给js端自行写入但那样需要调通.jslib插件层复杂度明显更高一般只有确认目标用户大量使用 Safari 时才值得做。5. 基于源码扩展 Boss 战、无尽模式与僚机的三个落点无论拿到的源码是纯代码还是带美术资源做扩展前先在工程里加三个空物体节点_Gameplay、_UI、_Managers把所有新增脚本按职责挂靠这是后期排查崩溃和循环引用的基础。第一个扩展落点是 Boss 战接入点。在 WaveSpawner 的逻辑末尾加一个判断当当前波次序号达到配置的 BOSS_WAVE 时暂停普通敌机生成调用单独的 BossController。Boss 的状态机推荐用整型相位比如 Phase 0 是入场Phase 1 是扇形弹幕Phase 2 是旋转弹幕每个相位切换时重新给角度数组赋值。子弹复用的对象池直接沿用普通子弹池只是把角度数组从敌人预制的每个子弹采样一次改成同一个数组循环采样。第二个落点是僚机跟随。玩家的火力升级往往伴随僚机系统。僚机不要直接写transform.position player.position offset那样会显得很机械。用Vector3.SmoothDamp做延迟跟随参数smoothTime设为 0.12 到 0.2 秒僚机才能保持略微滞后的运动轨迹。僚机射击复用手枪的射击方法把发射点替换成僚机自身位置即可不需要额外写开火逻辑。第三个落点是验证环节。每个扩展做完不要只看画面是否正常要开 Unity Profiler 的 CPU Usage 和 Memory 面板跑同一波敌人对比热点函数的耗时。重点看三处数据SetPass Call是否超过 30、GC Alloc是否在每帧产生非零的MonoBehaviour.Update分配、Instantiating字样是否频繁出现。如果帧率下降但数据没有异常升高优先怀疑是摄像机跟随或阴影相关设置而不是新加的代码逻辑。把这些验证方法固化成 checklist才能在多次扩展后仍然保持源码项目的运行底线。本文还有配套的精品资源点击获取