1. 项目概述:为什么“合成大西瓜”是绝佳的Unity 2D入门实战项目?
如果你对Unity游戏开发感兴趣,尤其是想从2D休闲游戏入手,那么“合成大西瓜”这个项目绝对是一个教科书级别的起点。我之所以这么说,是因为它麻雀虽小,五脏俱全。它看起来简单——不就是把两个相同的水果合成为一个更大的水果吗?但在这个简单的玩法背后,几乎涵盖了2D游戏开发的所有核心模块:物理引擎、碰撞检测、对象生成与销毁、UI交互、游戏状态管理、数据持久化,甚至还有一点简单的算法逻辑。对于新手来说,它能让你在成就感中快速上手;对于有一定基础的开发者,它又能让你深入思考如何优化代码结构、提升游戏手感。更重要的是,它的原型“合成大西瓜”曾风靡一时,证明了其玩法具有极强的用户粘性和传播性,理解它的开发过程,就等于掌握了一套打造爆款休闲游戏的底层方法论。
这个项目不依赖复杂的3D模型和光影,专注于2D精灵(Sprite)和逻辑,对硬件要求低,开发周期可控。我们将使用Unity 2022 LTS版本进行开发,这是目前最稳定且功能完善的长期支持版。整个指南将带你从零开始,一步步搭建项目框架,实现核心玩法,并最终打包成一个可以在手机上运行的APK文件。过程中,我会分享我踩过的坑和总结的最佳实践,确保你不仅能做出一个能玩的游戏,更能理解每一步背后的设计意图和实现原理。
2. 核心玩法拆解与游戏架构设计
在动手写代码之前,我们必须像建筑师看蓝图一样,把游戏的核心玩法和整体架构想清楚。盲目开干只会导致代码混乱,后期修改成本极高。
2.1 玩法机制深度解析
“合成大西瓜”的核心循环非常清晰:屏幕上方随机掉落水果 -> 玩家通过触屏(或鼠标)拖拽调整水果的掉落位置 -> 水果落地后发生物理碰撞 -> 两个相同等级的水果碰撞后合成一个更高等级的水果 -> 合成新水果时可能触发连锁反应 -> 游戏目标通常是合成出最高等级的水果(大西瓜),或者是在水果堆叠超过警戒线前尽可能获得高分。
这里有几个关键设计点需要深入思考:
- 物理模拟的真实性与游戏性平衡 :我们使用Unity的2D物理引擎(Rigidbody 2D, Collider 2D)来模拟水果的掉落、滚动和堆叠。但完全真实的物理可能会导致水果过于“滑”,难以精确控制落点。我们需要调整物理材质(Physics Material 2D)的摩擦力和弹性,或者在代码中施加一些阻尼,让手感更“粘稠”和可控。
-
碰撞检测与合成判定
:这是游戏逻辑的核心。不能简单地在
OnCollisionEnter2D里判断两个水果类型相同就立刻合成,因为那会导致多个水果挤在一起时发生错误的连续合成。更稳健的做法是:在碰撞发生时,将符合条件的“水果对”加入一个待处理列表,在物理更新之后、渲染之前的一个固定时间点(例如LateUpdate或使用协程延迟几帧)统一处理合成逻辑,确保每次合成事件都是清晰、独立的。 - 水果的等级与数据驱动 :我们需要一个清晰的数据结构来定义水果。最好的方式是用ScriptableObject来创建“水果配置表”。每个配置条目包含:水果等级(如1级樱桃,2级橘子…11级大西瓜)、对应的精灵(Sprite)、物理属性(质量、碰撞体大小)、合成后的分数等。这样,策划(或者你自己)调整游戏平衡性时,只需要修改这个配置文件,无需改动代码。
2.2 项目架构与文件夹规划
一个清晰的文件夹结构是项目可维护性的基石。在Unity中创建以下目录:
Assets/
├── _Scripts/ # 所有C#脚本
│ ├── Core/ # 核心管理器(GameManager, UIManager, SoundManager)
│ ├── Fruit/ # 水果相关脚本(Fruit, FruitSpawner, FruitData SO)
│ ├── Physics/ # 物理控制相关(DragController, Boundary)
│ └── UI/ # UI控制脚本(ScorePanel, GameOverPanel)
├── Art/ # 美术资源
│ ├── Sprites/ # 所有水果、背景、UI元素的精灵图
│ └── Materials/ # 材质球,如水果的物理材质
├── Prefabs/ # 预制体
│ └── Fruits/ # 各个等级的水果预制体
├── Scenes/ # 游戏场景
├── Settings/ # 项目设置文件(如InputSystem配置、Tag&Layer设置)
└── Resources/ # 需要运行时加载的资源(如配置表ScriptableObject)
为什么这么规划?
_Scripts
前面的下划线让它能排在前面,方便查找。按功能分文件夹,而不是按“场景”或“人物”分,能让相同职责的脚本聚集在一起,符合单一职责原则。将美术资源和预制体分开,方便美术人员独立工作而不影响场景。
3. 开发环境搭建与基础配置
工欲善其事,必先利其器。正确的初始配置能避免后续无数奇怪的问题。
3.1 Unity版本与关键包管理
我强烈推荐使用
Unity 2022.3 LTS
。长期支持版意味着更少的致命Bug和更长的维护周期。通过Unity Hub新建项目时,选择“2D核心模板”。创建后,第一件事是去
Window -> Package Manager
检查并更新关键包:
-
输入系统
:Unity的新输入系统(Input System Package)比旧的
Input类更强大、更灵活,尤其对于跨平台(PC、移动端)支持更好。务必安装它,并按照提示禁用旧输入系统。 -
2D相关包
:确保
2D Sprite,2D Physics等基础包已就绪。 - UI工具包(可选) :对于复杂的UI,可以了解UIToolkit,但对于我们这个项目,传统的uGUI(Canvas)完全够用且更直观。
3.2 场景与基础对象搭建
-
主摄像机(Main Camera)
:将Projection设置为
Orthographic(正交投影),这是2D游戏的标配。调整Size属性,确保游戏区域在编辑器中看起来舒适。我通常先设为5,后续根据实际美术资源再微调。 -
创建游戏区域
:创建一个空对象,命名为
GameArea。它的作用是容纳所有游戏动态对象。-
在其下创建
SpawnLine(一个空对象),放在屏幕上方,作为水果的生成线。 -
创建
Boundary(一个空对象),为其添加BoxCollider 2D组件,并勾选Is Trigger。将它拉伸成一个细长的矩形,放在屏幕底部偏下的位置。它的作用是检测掉出屏幕的水果,并将其销毁,防止内存泄漏。 注意 :碰撞体要足够“厚”,以防水果因高速穿透。
-
在其下创建
-
UI画布(Canvas)
:Unity会自动创建一个。将它的
Render Mode设置为Screen Space - Overlay,UI Scale Mode设置为Scale With Screen Size,参考分辨率可以设为1080x1920(竖屏),这样UI能在不同尺寸屏幕上自适应。
提示:为所有重要的空对象(如
SpawnLine,Boundary)在Inspector窗口的右上角点击小锁图标锁定它们,防止误操作。这是一个非常实用但常被忽略的小技巧。
4. 核心模块实现:水果系统
这是游戏的心脏,我们需要让水果“活”起来。
4.1 创建数据驱动的FruitData
在
Assets/Resources
文件夹下右键创建
FruitData
ScriptableObject。这是一个类,用于存储水果的静态属性。
// FruitData.cs
using UnityEngine;
[CreateAssetMenu(fileName = "New Fruit Data", menuName = "Fruit Data")]
public class FruitData : ScriptableObject
{
public int level; // 水果等级,从1开始
public Sprite sprite;
public float mass = 1f;
public float colliderRadius = 0.5f;
public int mergeScore; // 合成这个水果时获得的分数
public FruitData nextFruitData; // 指向合成后的下一个水果数据,形成链表
}
然后,你可以创建11个
FruitData
资产,分别对应樱桃、草莓、葡萄……直到大西瓜,并像链条一样将它们的
nextFruitData
属性串联起来。这种设计让合成逻辑变得极其简单:只要知道当前水果的数据,就能立刻知道它合成后会变成什么。
4.2 构建可交互的Fruit预制体
-
创建预制体
:在场景中创建一个Sprite,命名为
Fruit_Prefab_Template。为其添加以下组件:-
Sprite Renderer:精灵渲染器。 -
Circle Collider 2D:圆形碰撞体,勾选Is Trigger用于合成检测,不勾选用于物理碰撞(我们可能需要两个碰撞体,一个用于物理,一个用于触发合成,后面会讲)。 -
Rigidbody 2D:刚体。设置Body Type为Dynamic,Collision Detection为Continuous(防止高速穿透),Gravity Scale设为1,Linear Drag可以设为0.5增加一点空气阻力,让运动不那么“飘”。 -
创建一个物理材质(在Art/Materials文件夹),设置合适的摩擦力和弹性,并拖给碰撞体的
Material属性。
-
- 编写Fruit脚本 :将下面的脚本挂到预制体上。
// Fruit.cs
using UnityEngine;
public class Fruit : MonoBehaviour
{
[SerializeField] private FruitData _data; // 在Inspector中关联对应的FruitData
private Rigidbody2D _rb;
private bool _hasCollided = false; // 防止一帧内多次合成
private bool _isReadyToMerge = false; // 水果落地稳定后才允许合成
private void Awake() {
_rb = GetComponent<Rigidbody2D>();
GetComponent<SpriteRenderer>().sprite = _data.sprite;
GetComponent<CircleCollider2D>().radius = _data.colliderRadius;
_rb.mass = _data.mass;
}
private void OnCollisionEnter2D(Collision2D collision) {
// 与地面或其他物体碰撞后,短暂延迟后标记为“可合成”
if (!_isReadyToMerge) {
Invoke(nameof(SetReadyToMerge), 0.3f); // 0.3秒后稳定
}
}
private void OnTriggerEnter2D(Collider2D other) {
// 用于合成检测的触发器
if (!_isReadyToMerge || _hasCollided) return;
Fruit otherFruit = other.GetComponent<Fruit>();
if (otherFruit != null && otherFruit._isReadyToMerge && otherFruit._data.level == this._data.level) {
// 找到同等级且可合成的对象
_hasCollided = true;
otherFruit._hasCollided = true; // 防止对方也触发合成
GameManager.Instance.MergeFruits(this, otherFruit); // 通知GameManager处理合成
}
}
private void SetReadyToMerge() {
_isReadyToMerge = true;
}
public FruitData GetData() => _data;
}
关键点解析 :
-
使用
Invoke延迟设置_isReadyToMerge,是为了模拟水果落地后弹跳几下才稳定的真实感,避免还在空中翻滚时就合成,这很影响游戏体验。 -
_hasCollided标志位是解决“多重合成”问题的关键。当两个水果接触,双方都会触发OnTriggerEnter2D。如果没有这个标志,A碰到B触发一次,B碰到A又会触发一次,导致逻辑错误。我们让先触发的一方“标记”双方,然后统一由GameManager处理。 -
合成逻辑不放在
Fruit类内部,而是交给GameManager,是为了解耦。GameManager可以统筹分数更新、特效播放、声音播放等一系列后续操作。
4.3 实现水果生成器(FruitSpawner)
生成器负责在
SpawnLine
位置随机创建下一个水果。
// FruitSpawner.cs
using UnityEngine;
public class FruitSpawner : MonoBehaviour
{
[SerializeField] private FruitData[] _spawnableFruits; // 可生成的水果数据(通常是前几级)
[SerializeField] private Transform _spawnPoint;
[SerializeField] private GameObject _fruitPrefab;
[SerializeField] private float _spawnInterval = 1f;
private FruitData _nextFruitData;
private GameObject _nextFruitPreview; // 下一个水果的预览
private void Start() {
GenerateNextFruit();
InvokeRepeating(nameof(SpawnFruit), 1f, _spawnInterval);
}
public void SpawnFruit() {
if (_nextFruitData == null) return;
GameObject newFruit = Instantiate(_fruitPrefab, _spawnPoint.position, Quaternion.identity);
Fruit fruitComp = newFruit.GetComponent<Fruit>();
// 这里需要通过某种方式给Fruit脚本设置FruitData,可以修改Fruit脚本,增加一个Init方法。
fruitComp.Init(_nextFruitData); // 假设我们添加了Init方法
// 生成后,立即准备下一个
GenerateNextFruit();
}
private void GenerateNextFruit() {
if (_spawnableFruits.Length == 0) return;
_nextFruitData = _spawnableFruits[Random.Range(0, _spawnableFruits.Length)];
UpdatePreview();
}
private void UpdatePreview() {
// 销毁旧的预览
if (_nextFruitPreview != null) Destroy(_nextFruitPreview);
// 在生成点创建一个半透明的预览水果
_nextFruitPreview = Instantiate(_fruitPrefab, _spawnPoint.position, Quaternion.identity, _spawnPoint);
_nextFruitPreview.GetComponent<Fruit>().Init(_nextFruitData);
_nextFruitPreview.GetComponent<Rigidbody2D>().bodyType = RigidbodyType2D.Kinematic; // 设为运动学,不受物理影响
// 设置半透明材质或颜色
SpriteRenderer sr = _nextFruitPreview.GetComponent<SpriteRenderer>();
Color c = sr.color;
c.a = 0.5f;
sr.color = c;
}
// 提供给拖拽控制器调用,当玩家拖拽预览水果时
public FruitData GetAndConsumeNextFruit() {
FruitData dataToReturn = _nextFruitData;
GenerateNextFruit(); // 消耗后立即生成下一个
return dataToReturn;
}
}
这个生成器实现了自动周期生成和“下一个”预览功能。预览功能极大地提升了游戏的可玩性和策略性,是优秀休闲游戏的标配。
5. 核心模块实现:玩家交互与物理控制
游戏手感的好坏,很大程度上取决于拖拽控制的流畅度。
5.1 使用新输入系统实现精准拖拽
首先,在
Window -> Package Manager
中安装
Input System
包,并启用它。然后,在项目设置中创建一个新的Input Actions Asset,比如命名为
GameInput
。在里面定义一个
Touch
Action Map,添加一个
Position
(Vector2)类型的Action,用于读取触屏位置。
接着,创建拖拽控制器脚本:
// DragController.cs
using UnityEngine;
using UnityEngine.InputSystem;
public class DragController : MonoBehaviour
{
[SerializeField] private Camera _mainCamera;
[SerializeField] private FruitSpawner _spawner;
[SerializeField] private LayerMask _fruitLayer; // 只与水果预览层交互
[SerializeField] private float _dragSpeed = 15f;
private Vector2 _touchStartPos;
private GameObject _currentDraggedFruit;
private bool _isDragging = false;
private PlayerInput _playerInput;
private InputAction _touchPositionAction;
private void Awake() {
_playerInput = GetComponent<PlayerInput>();
_touchPositionAction = _playerInput.actions["Touch/Position"];
}
private void Update() {
if (!_isDragging && Touchscreen.current.primaryTouch.press.isPressed) {
StartDrag();
} else if (_isDragging && Touchscreen.current.primaryTouch.press.isPressed) {
ContinueDrag();
} else if (_isDragging && !Touchscreen.current.primaryTouch.press.isPressed) {
EndDrag();
}
}
private void StartDrag() {
Vector2 touchPos = _touchPositionAction.ReadValue<Vector2>();
_touchStartPos = _mainCamera.ScreenToWorldPoint(touchPos);
RaycastHit2D hit = Physics2D.Raycast(_touchStartPos, Vector2.zero, Mathf.Infinity, _fruitLayer);
if (hit.collider != null && hit.collider.CompareTag("NextFruitPreview")) {
_currentDraggedFruit = hit.collider.gameObject;
_isDragging = true;
// 从生成器获取下一个水果的真实数据,并实例化一个“正式”水果开始拖拽
FruitData data = _spawner.GetAndConsumeNextFruit();
// 在预览位置实例化正式水果,并销毁预览
Destroy(_currentDraggedFruit);
_currentDraggedFruit = Instantiate(_spawner.FruitPrefab, _touchStartPos, Quaternion.identity);
_currentDraggedFruit.GetComponent<Fruit>().Init(data);
_currentDraggedFruit.GetComponent<Rigidbody2D>().bodyType = RigidbodyType2D.Kinematic; // 拖拽时设为运动学
}
}
private void ContinueDrag() {
Vector2 currentTouchPos = _touchPositionAction.ReadValue<Vector2>();
Vector2 targetWorldPos = _mainCamera.ScreenToWorldPoint(currentTouchPos);
// 限制水平拖拽范围(不超过屏幕边界)
float clampedX = Mathf.Clamp(targetWorldPos.x, -2.2f, 2.2f); // 根据你的摄像机Size调整
targetWorldPos = new Vector2(clampedX, targetWorldPos.y);
_currentDraggedFruit.transform.position = Vector2.Lerp(_currentDraggedFruit.transform.position, targetWorldPos, Time.deltaTime * _dragSpeed);
}
private void EndDrag() {
if (_currentDraggedFruit != null) {
// 释放水果,恢复为动态刚体,受重力影响
Rigidbody2D rb = _currentDraggedFruit.GetComponent<Rigidbody2D>();
rb.bodyType = RigidbodyType2D.Dynamic;
// 可以给一个轻微的向下速度,让它更自然地掉落
rb.velocity = new Vector2(0, -1f);
_currentDraggedFruit = null;
}
_isDragging = false;
}
}
实操心得 :
-
使用
Lerp进行平滑移动 :直接transform.position = targetPos会显得生硬。使用Vector2.Lerp进行线性插值,能让拖拽手感更顺滑。_dragSpeed参数需要反复调试,在移动端通常需要更高的值来抵消触控采样延迟。 - 限制水平移动范围 :这是防止玩家把水果丢到屏幕外导致游戏体验断裂的关键。范围值需要根据你的游戏区域宽度动态计算,这里用了硬编码,更好的做法是根据摄像机视口边界计算。
-
拖拽时设为Kinematic
:这是关键技巧!如果拖拽时水果还是
Dynamic,它会受到重力和碰撞影响,导致拖拽手感极其糟糕,感觉像是在拖一个沉重的、乱晃的物体。设为Kinematic后,它完全由代码控制位置,手感轻盈精准。释放时再改回Dynamic。
5.2 优化物理与碰撞体验
物理手感是休闲游戏的灵魂。除了之前提到的物理材质,还有几个优化点:
-
碰撞体分层(Layer Collision Matrix)
:去
Edit -> Project Settings -> Physics 2D中,仔细设置碰撞矩阵。例如,让“正在拖拽的水果”层不与任何其他层碰撞(避免拖拽时卡住),只与“边界”层碰撞。水果落地后,再将其层改为“普通水果”,使其能与其他水果碰撞。这需要通过代码在拖拽开始和结束时动态改变物体的Layer。 -
使用复合碰撞体
:对于水果,可以附加两个
CircleCollider2D。一个稍大,设为Is Trigger = true,专门用于检测合成。另一个稍小,Is Trigger = false,用于物理碰撞和堆叠。这样既能精确检测合成,又不会因为触发碰撞体过大而导致水果物理上“粘”得太紧。 -
调整Time.fixedDeltaTime
:在
Project Settings -> Time中,Fixed Timestep默认是0.02秒(50次/秒)。对于手机游戏,尤其是物理运算较多的,可以尝试稍微调大,比如0.016667秒(60次/秒),能让物理更新更跟手,但会增加CPU负担,需要真机测试平衡。
6. 核心模块实现:游戏逻辑与状态管理
游戏管理器(GameManager)是游戏的大脑,它应该是单例(Singleton),方便其他脚本访问。
6.1 实现GameManager单例与合成逻辑
// GameManager.cs
using System.Collections.Generic;
using UnityEngine;
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[SerializeField] private FruitData[] _allFruitData; // 所有水果数据,用于根据等级查找
[SerializeField] private GameObject _mergeEffectPrefab;
private Dictionary<int, FruitData> _fruitDataDict = new Dictionary<int, FruitData>();
private int _currentScore = 0;
private void Awake() {
if (Instance != null && Instance != this) {
Destroy(gameObject);
} else {
Instance = this;
}
// 初始化数据字典
foreach (var data in _allFruitData) {
_fruitDataDict[data.level] = data;
}
}
public void MergeFruits(Fruit fruitA, Fruit fruitB) {
if (fruitA.GetData().level != fruitB.GetData().level) return;
int currentLevel = fruitA.GetData().level;
Vector2 mergePos = (fruitA.transform.position + fruitB.transform.position) / 2f; // 合成位置取中点
// 销毁两个旧水果
Destroy(fruitA.gameObject);
Destroy(fruitB.gameObject);
// 播放合成特效
if (_mergeEffectPrefab != null) {
Instantiate(_mergeEffectPrefab, mergePos, Quaternion.identity);
}
// 播放音效(这里需要接入你的AudioManager)
// AudioManager.Instance.PlayMergeSound();
// 增加分数
AddScore(fruitA.GetData().mergeScore);
// 生成新水果
if (_fruitDataDict.TryGetValue(currentLevel + 1, out FruitData nextData)) {
GameObject newFruit = Instantiate(/* 水果预制体 */, mergePos, Quaternion.identity);
newFruit.GetComponent<Fruit>().Init(nextData);
// 给新水果一个向上的小力,模拟合成弹跳效果
Rigidbody2D rb = newFruit.GetComponent<Rigidbody2D>();
if (rb != null) {
rb.AddForce(Vector2.up * 3f, ForceMode2D.Impulse);
}
} else {
// 已经是最大水果(如大西瓜)
Debug.Log("合成出了最大水果!");
}
}
private void AddScore(int score) {
_currentScore += score;
// 更新UI
UIManager.Instance.UpdateScore(_currentScore);
}
// 其他游戏状态管理方法,如游戏结束判断
public void CheckGameOver(float highestFruitY) {
if (highestFruitY > /* 你设定的警戒线Y坐标 */) {
UIManager.Instance.ShowGameOverPanel(_currentScore);
Time.timeScale = 0; // 暂停游戏
}
}
}
注意事项 :
-
单例模式的线程安全
:在Awake中实现简单的单例。确保场景中只有一个GameManager。更严谨的做法可以使用
Lazy<T>或服务定位器模式,但对于中小项目,这样足够了。 - 合成位置 :取两个水果位置的中点,看起来最自然。你也可以加入一点随机偏移,让合成不那么死板。
-
连锁合成处理
:上面的代码处理了一次合成。但合成出新水果后,如果新水果的落点旁边有另一个同等级水果,应该立刻再次合成。这需要递归或队列处理。一个简单的方法是,在生成新水果后,延迟几帧(
yield return new WaitForSeconds(0.1f)),然后检测新水果周围一定范围内是否有可合成的对象。如果有,再次调用MergeFruits。
6.2 实现UI管理器(UIManager)
UI管理器负责所有UI面板的显示、隐藏和更新。
// UIManager.cs
using UnityEngine;
using UnityEngine.UI;
public class UIManager : MonoBehaviour
{
public static UIManager Instance { get; private set; }
[SerializeField] private Text _scoreText;
[SerializeField] private GameObject _gameOverPanel;
[SerializeField] private Text _finalScoreText;
[SerializeField] private Button _restartButton;
private void Awake() {
if (Instance != null && Instance != this) {
Destroy(gameObject);
} else {
Instance = this;
}
_gameOverPanel.SetActive(false);
_restartButton.onClick.AddListener(OnRestartButtonClicked);
}
public void UpdateScore(int score) {
_scoreText.text = $"分数: {score}";
}
public void ShowGameOverPanel(int finalScore) {
_finalScoreText.text = $"最终分数: {finalScore}";
_gameOverPanel.SetActive(true);
}
private void OnRestartButtonClicked() {
// 重新加载当前场景
UnityEngine.SceneManagement.SceneManager.LoadScene(UnityEngine.SceneManagement.SceneManager.GetActiveScene().name);
Time.timeScale = 1; // 恢复时间流速
}
}
UI部分相对直接,重点是做好事件绑定(如按钮点击)和静态数据的显示。
7. 性能优化与移动端适配
当游戏基本功能完成后,我们需要让它能在手机上流畅运行。
7.1 对象池化(Object Pooling)
频繁地
Instantiate
和
Destroy
水果和特效是性能杀手,尤其是在低端移动设备上。对象池是解决这个问题的标准方案。它的核心思想是:预先创建一批对象,使用时从池中取出,不用时放回池中并禁用,而不是销毁。
我们可以创建一个简单的泛型对象池:
// SimpleObjectPool.cs
using System.Collections.Generic;
using UnityEngine;
public class SimpleObjectPool<T> where T : Component
{
private Queue<T> _pool = new Queue<T>();
private T _prefab;
private Transform _parent;
public SimpleObjectPool(T prefab, int initialSize, Transform parent = null) {
_prefab = prefab;
_parent = parent;
for (int i = 0; i < initialSize; i++) {
T obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
}
public T Get() {
if (_pool.Count > 0) {
T obj = _pool.Dequeue();
obj.gameObject.SetActive(true);
return obj;
} else {
// 池空了,动态创建一个(应尽量避免发生)
T obj = Object.Instantiate(_prefab, _parent);
return obj;
}
}
public void Return(T obj) {
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
}
然后在
FruitSpawner
和
GameManager
中,不再使用
Instantiate
和
Destroy
,而是从对应的对象池中
Get
和
Return
水果及特效。对于水果,需要在
Return
时重置其状态(如刚体速度、旋转、合成标志位等)。
7.2 移动端输入与性能调优
-
输入适配
:我们之前已经用了新的Input System,它本身对移动端支持很好。确保在
Player Settings -> Resolution and Presentation中,将Default Orientation设置为Portrait(竖屏)。同时,关闭Allowed Orientations中的横屏选项,强制竖屏。 -
图形优化
:
-
减少Draw Call
:确保所有水果精灵使用同一张图集(Sprite Atlas)。在
Window -> 2D -> Sprite Atlas中创建图集,将水果精灵拖进去。Unity在打包时会自动将小图合并成大图,极大减少Draw Call。 - 简化粒子特效 :合成特效尽量使用简单的粒子系统,控制最大粒子数(Max Particles)在50以下。
-
遮挡剔除(Occlusion Culling)
:对于2D游戏,可以手动实现简单的视口外对象禁用。写一个脚本挂在水果上,在
Update中判断transform.position.y是否远低于摄像机底部,如果是,则禁用渲染器GetComponent<SpriteRenderer>().enabled = false;,反之则启用。这能节省一些Overdraw。
-
减少Draw Call
:确保所有水果精灵使用同一张图集(Sprite Atlas)。在
-
代码性能
:
-
避免在Update中使用
Find或GetComponent:这些调用开销较大。在Awake或Start中缓存引用。 -
使用
CompareTag代替gameObject.tag == “Tag”:CompareTag是优化过的内部方法,效率更高。 -
物理优化
:如果水果数量很多(超过50个),可以考虑将静止的水果的刚体设为
Sleeping Mode(在Rigidbody 2D组件中设置),物理引擎会忽略它们,直到它们被再次唤醒。
-
避免在Update中使用
8. 打包发布与常见问题排查
最后一步,将你的游戏变成可以在手机上安装的APK文件。
8.1 Android平台打包设置
-
安装必要环境
:打开
Unity Hub,在你项目对应的Unity编辑器版本右侧,点击“添加模块”,确保已安装Android Build Support以及对应的OpenJDK和Android SDK & NDK Tools。Unity 2022通常已内置OpenJDK,但SDK路径可能需要指定。 -
Player Settings设置
:
-
Company Name和Product Name:填写你的公司和产品名。 -
Default Icon:设置应用图标。 -
Resolution and Presentation:如前所述,设为竖屏。 -
Other Settings:-
Identification->Package Name:格式必须为com.公司名.产品名,如com.YourStudio.MergeWatermelon。这是应用的唯一标识。 -
Configuration->Scripting Backend:选择IL2CPP。它比旧的Mono后端性能更好,且是发布到64位设备的必需项。 -
Target Architectures:勾选ARM64。现在主流手机都是64位,只勾选这个可以减小包体。 -
Minimum API Level:根据你的目标用户群设置,例如Android 8.0 (API Level 26)可以覆盖绝大多数设备。
-
-
-
Keystore设置
:如果你要发布到应用商店,需要用自己的密钥签名。在
Publishing Settings中,你可以创建一个新的或使用已有的Keystore。 务必保管好这个文件和相关密码! 丢失后将无法更新同一个应用。
8.2 常见打包错误与解决方案
-
错误:
CommandInvokationFailure: Failed to re-package resources:这通常是因为Android SDK路径不对或相关构建工具缺失。去Edit -> Preferences -> External Tools,检查Android SDK路径是否正确(通常位于C:\Users\[用户名]\AppData\Local\Android\Sdk)。确保已通过Android Studio的SDK Manager安装了SDK Platform对应API级别的包以及Build-Tools。 -
错误:
Unable to merge android manifests:可能是项目中不同的插件(如广告SDK、分析SDK)包含了冲突的Android清单配置。需要手动编辑Assets/Plugins/Android/AndroidManifest.xml文件来解决冲突,或者更新/移除有冲突的插件。 -
游戏在手机上安装后闪退(Crash)
:
-
检查日志
:将手机通过USB连接电脑,在Unity编辑器中打开
Window -> Analysis -> Profiler和Console。在手机上运行游戏,当闪退时,Profiler和Console可能会捕获到错误信息。更专业的方法是使用adb logcat命令在终端查看Android系统日志。 -
常见原因
:
- 内存不足 :在Profiler中查看内存使用情况,特别是纹理内存。优化图集,压缩纹理格式(使用ASTC)。
-
脚本错误
:可能是某个平台相关的代码在移动端报错。检查所有
Update、Start方法中是否有依赖PC特定输入(如鼠标)的代码。 -
IL2CPP Stripping
:如果使用了反射或者一些第三方库,IL2CPP的代码裁剪可能会误删必要的代码。尝试在
Player Settings -> Publishing Settings -> Managed Stripping Level中将其设为Low或Minimal。
-
检查日志
:将手机通过USB连接电脑,在Unity编辑器中打开
-
游戏在手机上运行卡顿
:
-
使用Unity Profiler连接真机进行性能分析。重点关注
CPU Usage和GPU Usage。 -
CPU瓶颈
:通常是物理计算(Physics)或脚本逻辑(Scripts)开销大。优化物理:减少刚体数量、使用更简单的碰撞体、调高
Fixed Timestep。优化脚本:避免每帧进行复杂的查找、使用对象池。 -
GPU瓶颈
:通常是填充率过高(Overdraw)。在
Camera上添加Universal Render Pipeline(如果使用URP)的Render Statistics组件,或使用Frame Debugger查看Draw Call和三角形数量。优化UI,避免全屏半透明叠加。
-
使用Unity Profiler连接真机进行性能分析。重点关注
完成这些步骤后,点击
File -> Build Settings
,选择
Android
平台,点击
Build
,选择一个输出目录,Unity就会开始编译打包。第一次打包可能会花费较长时间。成功后,你将得到一个APK文件,可以将其传输到安卓手机上进行安装测试。
整个开发流程走下来,你不仅完成了一个完整的游戏,更重要的是,你实践了从设计、编码、调试到优化、打包的全链路开发思维。记住,游戏开发是一个不断迭代和打磨的过程,第一个可玩版本只是起点。接下来,你可以考虑加入更多元素,比如不同的游戏模式、道具系统、更精美的动画和音效、积分排行榜,甚至简单的AI对手,让这个“大西瓜”变得更加丰满和有趣。

1005

被折叠的 条评论
为什么被折叠?



