尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity3D篮球游戏Demo:物理引擎、Rigidbody与投篮算法实战解析

Unity3D篮球游戏Demo:物理引擎、Rigidbody与投篮算法实战解析 简介一个基于Unity3D开发的简易篮球游戏Demo面向Unity初学者与游戏开发爱好者可作为从零理解3D游戏交互逻辑的入门参考。项目覆盖场景构建、篮球投篮与反弹的物理模拟、C#脚本控制得分、Animator动画、UI计分面板及音效播放等核心环节适合边看边改、动手实践。资源共1207个文件压缩包约46.65MB以meta与info等Unity工程辅助文件居多同时包含fbx模型、mat材质、png纹理、cs脚本、prefab预制体、mp3音频和shader着色器等典型资源完整度较高。已有949人学习浏览。通过学习该Demo可了解Unity场景组织、刚体与碰撞参数调整、脚本事件响应、动画状态机设置及UI动态更新等常见实现方式为后续独立开发小游戏打下基础。1. 篮球游戏Unity3D Demo最值得动手复刻的第一个物理小项目打开Unity Hub新建一个3D项目往场景里放一颗球、一个篮筐让球能投进去——这个“简单的篮球游戏”的Unity3D Demo核心逻辑不到两百行却几乎把Unity日常开发要用的积木全过了一遍Rigidbody物理、Collider与Trigger、输入与相机、UI与回合循环。很多人把它当大学作业交差也有人拿它验证新版本Unity的物理API有没有变化。对一个想把Unity3D从“看过教程”推到“能自己动手做东西”的人篮球Demo是性价比最高的练习它比Cube滚动的Hello World多一点目标约束又比完整商业项目少掉美术、联网和资源管理的负担做完之后你对“一个游戏是怎么从空场景长出来的”会建立完整认知。我做完第一版后最大的感受是它逼你把物理参数、输入坐标转换和状态管理三件事同时想清楚任何一处偷懒都会在“投篮不进”里暴露出来。这篇按我的实现顺序展开场景搭建、物理参数、投篮算法、计分回合一章一章给代码和调试思路最后是踩坑清单新手可以照着搭熟手可以直接跳到自己关心的参数和边界。2. 先把场景和物理立住尺寸、刚体、重力怎么配才不玄学2.1 用Unity内置几何体搭出“地板、篮架、篮球”最小集合不需要下载任何模型Unity内置的Cube、Sphere、Cylinder就够了。我习惯按下面的步骤搭顺序固定下来不容易漏碰撞体新建3D场景保留默认的Directional Light加一个Plane作为地板位置(0, 0, 0)。在地板一侧放一个Cube作为篮架立柱Scale设为(0.15, 3, 0.15)位置(0, 1.5, 0)。在立柱顶部放一个Cube作为篮板Scale设为(2, 1.2, 0.08)位置(0, 3.2, -0.1)。在篮板下方做一个Cylinder作为篮筐的视觉环Scale设为(1.2, 0.05, 1.2)位置(0, 2.6, 0)半透明材质。在篮筐中心再放一个Cylinder作为得分触发器Scale设为(0.9, 0.04, 0.9)位置(0, 2.58, 0)勾选IsTrigger。加一个Sphere作为篮球Scale设为(0.5, 0.5, 0.5)放在篮筐前方6米、高度0.3米的位置。这里有个关键选择内置几何体里没有Torus所以篮筐环用压扁的Cylinder替代视觉上是一个圆盘。很多教程会让这个圆盘同时承担碰撞和得分判定这是后面所有问题的根源。我一般把“视觉篮筐”和“得分触发器”分开视觉环保留Collider球碰到环会弹跳真实触发器缩到比环内径小一圈只负责判定球有没有真正穿过筐口。这样球在筐沿蹦跳时不会误判得分。尺寸比例也要注意。这个配置下球半径为0.25米篮筐视觉环半径0.6米直径1.2米球筐比例约2.4:1。真实篮球直径约24厘米、篮筐直径约45厘米比例接近1.9:1游戏里把比例放宽到2.2~2.5:1新手才投得进。如果你有用SolidWorks之类的CAD工具导入篮架模型时记得确认FBX的Scale Factor和单位否则模型缩成蚂蚁或变成巨兽物理尺寸的坑比想象中常见。2.2 刚体与碰撞体参数质量、Drag、物理材质篮球需要挂Rigidbody立柱、篮板、地板都不挂只保留默认Collider作为静态碰撞体。篮筐视觉环有Collider但不挂Rigidbody这样球撞上去会弹开而环本身不动。下面这个脚本挂在球上在Awake里把物理参数统一设置好避免每次新建项目都手点一遍using UnityEngine; public class BallPhysicsSetup : MonoBehaviour { public float mass 1f; public float bounciness 0.4f; public float drag 0.05f; private Rigidbody rb; void Awake() { rb GetComponentRigidbody(); rb.mass mass; rb.drag drag; rb.angularDrag 0.05f; rb.collisionDetectionMode CollisionDetectionMode.Continuous; rb.interpolation RigidbodyInterpolation.Interpolate; PhysicMaterial mat new PhysicMaterial(Ball); mat.bounciness bounciness; mat.dynamicFriction 0.4f; mat.staticFriction 0.4f; mat.frictionCombine PhysicMaterialCombine.Average; mat.bounceCombine PhysicMaterialCombine.Average; GetComponentCollider().material mat; Debug.Log($Gravity: {Physics.gravity.y}, Mass: {rb.mass}); } public void TestTrajectory(Vector3 velocity) { rb.velocity velocity; } }几个参数为什么这么设。mass设1而不是真实篮球的0.6kg投篮Demo里没有需要精确模拟质量的场景mass大一点让球撞筐后速度损失更小手感更稳。drag设0.05是刻意的超过0.2球会明显“飘不起来”斜抛计算结果也会和实际落点偏差越来越大这是很多“球总投不到目标”的玄学来源。angularDrag保持0.05空中旋转不会太疯。collisionDetectionMode改成Continuous是为了防高速穿模代价是少量性能对小场景无所谓。物理材质这里有个容易记错的地方Unity里两个碰撞体的bounciness组合方式默认为Average不是Multiply。如果只给球挂了bounciness0.4的材质地板不挂材质按0处理平均后只有0.2弹得不高不低看起来“说不清”。所以球、地板、篮板、视觉筐环都挂同一个PhysicMaterial值与值一致弹力表现才可预期。摩擦不要设成0否则球落地像冰球一样滑走。参数汇总如下物体组件参数推荐值球Rigidbodymass / drag / angularDrag1 / 0.05 / 0.05球RigidbodyCollision DetectionContinuous球RigidbodyInterpolationInterpolate球 地面PhysicMaterialbounciness / friction0.4 / 0.4全局Project SettingsPhysics.gravity.y-12全局Project SettingsFixed Timestep0.022.3 重力和固定时间步长为什么你的球总是“软绵绵”在Edit Project Settings Physics里默认Gravity y-9.81。投篮Demo的第一感觉往往是“球飘”原因不是重力小而是Drag偏大、弧线公式没有跟着重力走。我一般把重力调到-12到-13球下落更快单位时间能多投几次也更接近街机投篮机的节奏。这里有一个必须遵守的约定脚本里凡是算抛物线一律用Physics.gravity.y不要写死9.81。血泪经验是第一版把重力改成-12后瞄准点整体下沉了快半米排查半天发现代码里写死了9.81。这个习惯越早养成越好。Fixed Timestep默认0.02秒不要图“更精确”改成0.01。对小篮球Demo来说球速通常在5~15米/秒0.02秒步长下每帧最多走0.3米用Continuous碰撞检测足够覆盖把Fixed Timestep调小会成倍增加物理计算量手机上进帧率反而更不稳定。Interpolation务必要开否则物理按固定步长更新、渲染按帧率走球看起来会一顿一顿。3. 投篮的本质是初速度用斜抛公式实现准的出手3.1 为什么用velocity赋值而不用AddForce投篮是一次性事件不是持续施力。AddForce每帧累加受质量和帧率影响想调出一个固定弧线要反复试Rigidbody.velocity直接给初速度一帧生效和物理公式完全对应是Demo里最可预测的方案。如果坚持用AddForce正确姿势是rb.AddForce(v * rb.mass, ForceMode.Impulse)冲量等于质量乘速度增量物理上等价于设velocity但多绕一次质量换算没必要。判断标准很简单你想要的是一条能用公式算出来的抛线还是依赖物理累积的随机感。篮球投篮的落点必须可控所以选velocity。3.2 用斜抛公式反推初速度给目标点和飞行时间已知起点S、目标点T、飞行时间Tf、重力g负数水平方向的速度是位移除以时间垂直方向要把重力在Tf时间内产生的位移减掉v_xz (T - S).xz / Tf v_y (T.y - S.y) / Tf - 0.5 * g * Tf对应脚本using UnityEngine; public class Shooter : MonoBehaviour { public Transform ball; public Transform hoop; public float flightTime 1.0f; public void ShootTowardsHoop() { Rigidbody rb ball.GetComponentRigidbody(); Vector3 start ball.position; Vector3 target hoop.position; float g Physics.gravity.y; Vector3 v (target - start) / flightTime; v.y - 0.5f * g * flightTime; rb.velocity v; } }flightTime是手感核心参数推荐0.9到1.1秒。小于0.6秒球路太平太直飞行中玩家看不清弧线还容易磕前筐大于1.4秒球会抛得很高视觉上很飘落点误差被放大。起点和篮筐水平距离6米左右时flightTime1.0的弧线最舒服。注意这个公式建立在drag≈0的前提上。drag0.05时偏差可以忽略drag超过0.2球的实际落点会比公式明显偏近你会看到预览线说“进了”球却落在筐前。所以调投篮前先确认球的drag别让空气阻力偷偷吃掉速度。3.3 划屏投篮把手指滑动折算成世界空间速度手机上更自然的投篮方式不是点按钮而是按住屏下方向上滑滑得越快球投得越远。关键是把屏幕坐标delta正确折算成世界方向这里翻车率极高。using UnityEngine; public class SwipeShooter : MonoBehaviour { public Transform ball; public float powerScale 4f; public float minPower 2f; public float maxPower 12f; public float liftAmount 1.2f; private Vector2 startPos; void Update() { if (Input.GetMouseButtonDown(0)) { startPos Input.mousePosition; } if (Input.GetMouseButtonUp(0)) { Vector2 delta Input.mousePosition - startPos; if (delta.magnitude 30f) return; float power Mathf.Clamp(delta.magnitude * powerScale, minPower, maxPower); Vector3 fwd Camera.main.transform.forward; fwd.y 0f; fwd.Normalize(); Vector3 dir (fwd * delta.y Camera.main.transform.right * delta.x).normalized; Vector3 velocity dir * power; velocity.y liftAmount; ball.GetComponentRigidbody().velocity velocity; } } }逻辑拆开看powerScale把像素滑动距离换算成出手力度30像素以下的滑动当成误触直接忽略。方向映射分两步先把相机forward去掉Y分量作为水平基准再和相机right组成一个水平面坐标系屏幕向上滑delta.y为正对应“向前上方出手”屏幕左右滑对应“横向瞄准”。最后加一个固定向上抬升量liftAmount避免玩家划得太平导致球直接撞篮板。这个映射不自动瞄准篮筐玩家通过调整滑动手感来掌握弧线这是Demo该有的样子。如果想让球朝篮筐自动修正把速度方向往(hoop.position - ball.position)方向拉一点就行但那样玩起来像外挂不建议这么做。4. 得分判定与回合循环触发器、UI和复位逻辑4.1 进球检测为什么不能用OnCollisionEnter触发器和方向判断球碰到篮筐环是“碰撞”球进入筐口是“穿过”这是两类完全不同的事件。如果只用OnCollisionEnter球碰一下筐就加分游戏没法玩。我在2.1里放的第二个Cylinder就是专门用来检测“穿过筐口”的触发器。using UnityEngine; using System.Collections; public class ScoreDetector : MonoBehaviour { public GameManager manager; void OnTriggerEnter(Collider other) { if (!other.CompareTag(Ball)) return; Rigidbody rb other.GetComponentRigidbody(); if (rb.velocity.y 0f) return; manager.OnScore(); StartCoroutine(Cooldown()); } IEnumerator Cooldown() { GetComponentCollider().enabled false; yield return new WaitForSeconds(0.8f); GetComponentCollider().enabled true; } }两个条件缺一不可。CompareTag(Ball)保证只有球能触发先给球在Inspector里加Ball标签。rb.velocity.y 0f表示只认下落状态的球从下往上穿筐不计分空中上抛经过筐口也不计分。Cooldown防止球进筐后在触发器里弹跳、一次投篮触发多次加分。触发器尺寸和位置是这套判定的边界条件我建议触发器半径取视觉筐环内径的80%~90%即0.45米左右高度0.08~0.15米。球半径0.25米球心进入触发器时大半个球已经在筐内符合“投进了”的直觉。触发器太大容易擦筐沾边就加分太小则进球明明穿过筐口却漏判。4.2 用GameManager统一得分、倒计时和球复位得分之后要处理三件事分数累加、球复位、倒计时继续走。复位不能瞬间发生否则玩家看不到进球后球落地的反馈体验很差。用协程延迟0.8秒再复位。using UnityEngine; using UnityEngine.UI; using System.Collections; public class GameManager : MonoBehaviour { public Text scoreText; public Text timeText; public Transform ballSpawn; public Transform ball; private int score; private float timeLeft 60f; void Start() { ResetBall(); } void Update() { timeLeft - Time.deltaTime; if (timeLeft 0f) timeLeft 0f; timeText.text 时间: Mathf.CeilToInt(timeLeft) s; if (ball.position.y -5f) { ResetBall(); } } public void OnScore() { score; scoreText.text 得分: score; StartCoroutine(ResetAfter(0.8f)); } IEnumerator ResetAfter(float delay) { yield return new WaitForSeconds(delay); ResetBall(); } void ResetBall() { Rigidbody rb ball.GetComponentRigidbody(); rb.velocity Vector3.zero; rb.angularVelocity Vector3.zero; ball.position ballSpawn.position; ball.rotation Quaternion.identity; } }出界判断的y界限-5是相对于地板y0说的如果地板做了下沉或场景高度不同这个值要跟着调。注意复位时清的是velocity和angularVelocity光把球传回发球点残留的角速度会让球落地后自己滚走这是很常见的遗漏。时间归零后记得加一个isGameOver标志位在Update里屏蔽投篮输入和划屏否则倒计时结束还能继续投逻辑就穿帮了。4.3 按钮瞄准与划屏瞄准的切换方式同时保留两种投篮模式时最常见的问题是按钮和触摸在同一帧互相抢输入。我的做法是把Shooter和SwipeShooter分别挂在两个空物体上GameManager里加一个public bool useButtonMode在Start里根据这个开关Enable或Disable对应脚本。按钮模式的Shooter会直接朝篮筐把球投出去适合用在“练习模式”或“自动回放”场景划屏模式保留给玩家实际操作。两个脚本同时enable会在一次点击里触发两次赋值最后谁后执行谁生效表现就是方向时对时错调起来很恼人。5. 新手最容易踩坑的5个点从球穿筐到点击失灵这个Demo看似简单真正动手做一遍能炸出来的问题比想象多。以下按我复用这个Demo时遇到的真实现象整理每条都是“现象 → 原因 → 解决”三段。5.1 碰撞与物理相关球穿筐、球不弹、球在筐沿跳舞现象一球高速穿过篮板或视觉筐环直接掉到地上。 原因Rigidbody默认Collision Detection是Discrete每物理帧只做一次离散碰撞检测球速快、碰撞体薄时一帧内球已经从前到后穿过去了。篮板厚度只有0.08米就是重灾区。 解决球设Continuous篮板加厚到0.12~0.2米绝对不要给篮筐视觉环用MeshCollider薄片MeshCollider是穿模之王。现象二球落地基本不弹像砸在水泥地上。 原因只给球挂了PhysicMaterial地面没有材质bounciness按Average组合后被默认的0拉低或者球的drag设得太大速度被空气阻力吃光了。 解决球、地板、篮板、视觉筐环都挂同一个PhysicMaterialbounciness0.4、friction0.4drag回到0.05。不要再单独调大bounciness到0.9弹力过强会出现现象三。现象三球掉在筐沿上疯狂跳动怎么都不进。 原因bounciness太高且摩擦系数太低球每次碰撞的反弹角度都足够大在圆环上反复弹跳。视觉筐环碰撞体如果和得分触发器重叠还会出现球被“卡”在环里的观感。 解决bounciness降到0.4friction保持在0.4让球碰到筐沿后速度有明显损失同时把得分触发器半径缩到视觉环内径的90%以下两者错开。这个参数组合是我试过最稳定的区间再往上调就是纯粹的“筐沿跳舞”表演。5.2 输入与UI相关按钮点不到、划屏乱飞、误触得分现象四运行后UI按钮怎么点都没反应但游戏逻辑在走。 原因Canvas下每个Image和Text默认都勾选Raycast Target一个全屏半透明Image挡在按钮上面把所有点击都吞了或者场景里压根没有EventSystem。 解决把所有纯展示用的Image和Text的Raycast Target取消勾选只给Button保留检查Hierarchy里有EventSystem并且Standalone Input Module没有被误删。现象五手机上向上划屏球往相机右侧乱飞。 原因直接把屏幕delta映射成世界向量或用Camera.main.transform.TransformDirection(Vector3.up)——相机up在侧视角下是斜的屏幕的“上”和世界的“斜上方”完全不是一回事。 解决按3.3节的写法先用相机forward去掉Y分量做水平基准再结合相机right组成水平面坐标系。方向映射必须先降维到水平面再重建世界方向不能直接拿屏幕坐标当世界坐标。另外一个坑是Editor和真机分辨率不同灵敏度必须真机测。现象六球滚进筐后一次投篮加好几十分。 原因球在触发器内停留或弹跳OnTriggerEnter被多次触发。 解决ScoreDetector加0.8秒冷却协程ResetAfter和Cooldown双保险同时确认触发器半径没有比球大太多否则球还没进筐中心就触发加分。5.3 公式与重力相关改了重力全偏、预览线和落点不符现象七把Project Settings里的重力从-9.81改成-12后原来的投篮公式全部偏高球永远飞过目标。 原因脚本里写死了9.81或者把重力当常数放在公式里没有读Physics.gravity.y。 解决统一用float g Physics.gravity.y每调一次Project Settings的Gravity就在代码里搜一遍9.81这是成本最低的排查方式。现象八用抛物线公式算出的初速度实际投出去总是差一点。 原因球Rigidbody的drag不是0公式忽略空气阻力速度越高偏差越大或者球在飞行中被其他脚本额外AddForce过比如镜头抖动脚本。 解决drag调到0.05以下检查球身上所有脚本确保飞行途中没有任何外力写入。验证方法参考第6章的预览线如果预览和实际落点对不上先怀疑外力再怀疑drag。6. 把Demo做扎实抛物线预览线调试与移动端手感优化6.1 把看不见的弹道画出来投篮手感为什么难调因为出手瞬间你根本看不见球的未来轨迹只能靠一次次试投猜参数。我的做法是在球旁边挂一个预览脚本把当前速度对应的抛物线用Debug.DrawLine实时画出来using UnityEngine; public class TrajectoryPreview : MonoBehaviour { public Transform ball; public float previewTime 1.5f; public float step 0.05f; void Update() { Rigidbody rb ball.GetComponentRigidbody(); Vector3 start ball.position; Vector3 velocity rb.velocity; float g Physics.gravity.y; Vector3 last start; for (float t 0f; t previewTime; t step) { Vector3 p start velocity * t 0.5f * g * t * t * Vector3.up; Debug.DrawLine(last, p, Color.green, 0.02f); last p; } } }验证方法很直接给球一个固定初速度比如(0, 6, 8)看预览线终点和实际落点是否重合。重合说明drag、重力、公式三者一致不重合就调drag或检查外力。这套方法的价值在于把“手感玄学”变成可观测的对照调参效率高出一个量级。6.2 让我后悔没早点做的三件事相机参数主相机用透视模式FOV固定在45左右放在篮筐侧前方约(6, 4, -7)LookAt篮筐中心。FOV太大球在屏幕上显得很远FOV太小视野窄找不到球在哪。这个位置和角度能让投篮弧线完整呈现在屏幕中央。Canvas设置UI文本用TextMeshPro把Canvas Scaler设为Scale With Screen Size参考分辨率按目标机型设置比如1080x1920。默认Canvas缩放模式在手机上会糊投篮位置判断也会因UI缩放变得不准确。性能与音效手机上锁Application.targetFrameRate 60否则物理插值在高低帧率切换时还是会卡。给球挂一个AudioSource在OnCollisionEnter里判断碰撞对象是地板还是篮板播放不同的弹地音效手感提升非常明显注意不要在OnCollisionEnter里Instantiate音频对象复用同一个AudioSource改pitch就行。最后说一个我自己的教训最早做这个Demo时把bounciness调到0.9球在筐沿弹了半分钟都没进去后来把所有碰撞体统一成同一个PhysicMaterial、bounciness回到0.4才恢复正常。从那以后每次调物理参数第一件事就是确认脚本里有没有写死重力和drag。这个Demo虽然叫“简单”但它把物理、输入、状态管理三件事串在了一起能一遍跑通你的Unity3D基本功基本就立住了。希望帮到你。本文还有配套的精品资源点击获取
返回列表