在2D游戏开发中,物理引擎虽然方便,但有时候我们需要更精细的控制和更高的性能。Pong这类经典弹球游戏就是绝佳的练习场景——它规则简单,但实现起来却暗藏玄机。本文将带你绕过Unity物理引擎,用纯代码实现一套轻量级碰撞检测系统。
Unity自带的物理引擎(Rigidbody和Collider)确实能快速实现碰撞效果,但在某些场景下会成为负担:
典型性能对比:
| 实现方式 | 内存占用 | CPU耗时 | 可控性 |
|---|---|---|---|
| 物理引擎 | 较高 | 3-5ms | 一般 |
| 手写检测 | 极低 | <1ms | 完全可控 |
提示:当游戏对象少于50个时,手写方案的性能优势会非常明显
实现墙壁反弹只需要基本的矩形边界判断:
csharp复制// 假设游戏区域为800x600像素
float screenWidth = 800f / pixelScale;
float screenHeight = 600f / pixelScale;
void CheckWallCollision()
{
Vector2 ballPos = ball.transform.position;
// 左右墙碰撞
if(Mathf.Abs(ballPos.x) > screenWidth/2)
{
ballSpeedAngle = Mathf.PI - ballSpeedAngle;
// 得分逻辑...
}
// 上下墙碰撞
if(Mathf.Abs(ballPos.y) > screenHeight/2)
{
ballSpeedAngle = -ballSpeedAngle;
}
}
普通矩形检测会导致生硬的反弹效果,我们可以改进为:
csharp复制bool CheckPaddleCollision(Vector2 paddlePos)
{
Vector2 ballPos = ball.transform.position;
// 基础矩形检测
if(!(ballPos.x > paddlePos.x - paddleWidth/2 &&
ballPos.x < paddlePos.x + paddleWidth/2 &&
ballPos.y > paddlePos.y - paddleHeight/2 &&
ballPos.y < paddlePos.y + paddleHeight/2))
{
return false;
}
// 计算碰撞点相对于挡板中心的偏移比例 (-1到1)
float hitFactor = (ballPos.y - paddlePos.y) / (paddleHeight/2);
// 根据碰撞位置调整反弹角度
ballSpeedAngle = Mathf.PI/2 * hitFactor + (ballPos.x > 0 ? Mathf.PI : 0);
return true;
}
极坐标转直角坐标(适合固定速度):
csharp复制float speedX = Mathf.Sin(angle) * speed;
float speedY = Mathf.Cos(angle) * speed;
向量叠加(适合变速运动):
csharp复制Vector2 velocity = new Vector2(1, 0.5f).normalized * speed;
物理模拟(带加速度):
csharp复制velocity += acceleration * Time.deltaTime;
position += velocity * Time.deltaTime;
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 球卡在边界 | 碰撞检测顺序错误 | 先移动再检测 |
| 反弹角度异常 | 角度计算未标准化 | 使用Mathf.Clamp限制范围 |
| 移动不流畅 | 未考虑Time.deltaTime | 所有位移乘以Time.deltaTime |
建议采用分层设计:
csharp复制public class PongGame : MonoBehaviour
{
// 游戏对象引用
[SerializeField] Transform ball;
[SerializeField] Transform[] paddles;
// 游戏参数
float ballSpeed = 12f;
float paddleSpeed = 8f;
Vector2 ballVelocity;
void Update()
{
HandleInput();
MoveBall();
CheckCollisions();
UpdateScore();
}
void HandleInput()
{
// 处理玩家输入控制挡板
}
void MoveBall()
{
ball.position += (Vector3)ballVelocity * Time.deltaTime;
}
void CheckCollisions()
{
CheckWallCollision();
CheckPaddleCollision();
}
}
注意:所有位置计算建议使用Unity的本地坐标系,避免直接使用像素值
通过射线检测提前预判碰撞,避免穿墙:
csharp复制Vector2 nextPos = ballPos + velocity * Time.deltaTime;
if(Physics2D.Linecast(ballPos, nextPos))
{
// 计算精确碰撞点
RaycastHit2D hit = Physics2D.Raycast(ballPos, velocity.normalized);
ballVelocity = Vector2.Reflect(velocity, hit.normal);
}
对频繁创建销毁的对象(如特效),使用对象池:
csharp复制public class ObjectPool : MonoBehaviour
{
public GameObject prefab;
public int poolSize = 10;
Queue<GameObject> pool = new Queue<GameObject>();
void Start()
{
for(int i=0; i<poolSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
pool.Enqueue(obj);
}
}
public GameObject GetObject()
{
if(pool.Count > 0)
{
GameObject obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
return Instantiate(prefab);
}
}
在Scene视图绘制辅助线:
csharp复制void OnDrawGizmos()
{
// 绘制游戏边界
Gizmos.color = Color.green;
Gizmos.DrawWireCube(Vector3.zero, new Vector3(screenWidth, screenHeight, 0));
// 绘制球体运动轨迹
Gizmos.color = Color.red;
Gizmos.DrawLine(ball.position, ball.position + (Vector3)ballVelocity);
}
关键性能计数器实现:
csharp复制float deltaTime;
float fps;
float ms;
void Update()
{
deltaTime += (Time.deltaTime - deltaTime) * 0.1f;
fps = 1.0f / deltaTime;
ms = deltaTime * 1000.0f;
DebugText.text = $"FPS: {fps:0.} MS: {ms:0.0}";
}
在实现过程中发现,当游戏逻辑全部放在Update中时,性能损耗会比分散到FixedUpdate高约15%。建议将物理计算移到FixedUpdate,渲染相关操作留在Update。