☰
Unity3D篮球投篮游戏Demo开发实战:物理碰撞与C#脚本实现
2026/10/6 10:14:03 网站建设 项目流程

简介:一款基于Unity3D的篮球游戏Demo,面向Unity初学者和3D游戏开发爱好者,旨在通过一个可运行的投篮项目,展示从场景搭建、模型材质到物理交互、脚本控制的完整游戏制作流程。压缩包共收纳1207个文件,总大小约46.65MB,其中包含大量元数据(info与meta)、材质(mat)、二进制资源(bin)、C#脚本、Unity场景、预制体、FBX模型及MP3音频等类型,目录结构清晰,便于按模块对照学习。目前已有949人学习下载。该Demo覆盖Unity3D多个核心环节:通过场景管理切换游戏状态;利用刚体与碰撞模拟投篮、反弹;使用C#脚本处理玩家输入与分数更新;借助动画控制器实现球员庆祝等动作;并通过UI系统制作计分板与菜单。同时涉及材质纹理、音频播放和性能优化设置,适合作为从零开始创建3D游戏的学习范本,也可用于教学演示或二次开发。

1. 一个简单的篮球游戏Unity3d Demo:半小时跑通投篮循环,值得复现的入门样本

如果只在Unity3d里摆一个球和一块篮板,点一下Play看它自己弹跳,那不叫篮球游戏。真正让我觉得这个Demo能起项目了,是第一次在屏幕上向后划动手指、球顺着弧线落进筐里、记分牌跳数字的那一瞬间。标题里的“简单”不是偷工减料,而是把投篮、进筐、计分、重置四个环节用最直白的方式串起来——不需要美术资源,不需要行为树,一个空场景加三个脚本就能跑。这个方向适合刚学Unity、想在简历上放一个可玩Demo的新人,也适合想快速验证投篮手感想法的策划。按下面的路子复现一遍,你会发现篮球游戏的骨架比想象中小。

2. 拆解Demo的四个核心模块:场景搭建、物理碰撞、计分检测、UI反馈

2.1 篮球场的最小场景:不用美术资源也能搭

先用引擎原生基本体把场地立起来。Unity单位按米算,篮球标准直径约0.246米,所以Hierarchy右键创建Sphere,把Scale设成0.3左右,视觉上就是一颗球。场地直接用Plane拉大到5x5米。篮筐用一个空物体挂上Unity自带的Torus(圆环)当框体,我一般取outerRadius=0.25、innerRadius=0.2,配合篮板Cube,Scale给(1, 0.8, 0.1),竖在圆环后面,再用Cylinder从篮板下方接到地面做支撑。这套组合不到五分钟就能搭完,而且已经具备碰撞和视觉辨识度。

为什么坚持用基本体而不是下载模型:Unity对Sphere、Box、Capsule这类原生碰撞体的检测成本远低于Mesh Collider。Demo阶段场景里只有篮球和篮筐两个动态交互,把篮筐拆成基本体,球碰到任何位置都不会出现“穿过三角面片缝隙”的诡异行为,也避免了一开始就背着美术资源跑。很多新手上来就导入一个高模篮球场,结果光修Mesh Collider就修了两天,这属于典型的把Demo做成了项目。

摆放坐标建议以出手点为原点:球生成在(0, 0.8, 0),篮板中心放在(0, 3, 0)左右,篮筐圆环底部略高于篮板中线,让玩家从侧面看时能看到筐。摄像机的角度比位置更难调:我习惯把相机拉到篮筐侧后方约37度俯角,保证投球弧线的最高点出现在画面中央。这样玩家拖拽时对力度的感知最直观,球路几乎全程落在视野里,不会出现球飞到画面外还要靠感觉判断的状况。

2.2 投篮的物理模型:刚体和碰撞器的组合拳

篮球要飞起来,必须挂Rigidbody。这是Unity物理的入口,所有受力、碰撞、旋转都走它。新弹出的Rigidbody默认参数可以直接用,但要把下面几项先改掉,否则后续手感全不对。我给的起点参数是:Mass=0.6,对应真实篮球约600g;Drag=0.05,Angular Drag=0.1;Interpolate改成Interpolate,避免低帧率下球在视觉上抖动。

再看Collider。Sphere Collider的Radius保持1,配合Sphere缩放后的最终尺寸就是球的大小,一般不用改。真正要建的是Physic Material:在Project窗口右键Create > Physic Material,命名为BallBounce,Bounciness=0.7,Bounce Combine选Maximum,Friction留0.4。把这份物理材质拖到篮球的Sphere Collider的Material槽,篮板和篮筐也可以共用一份,球打板后的回弹才自然。

这里有个容易误会的点:Physic Material只影响碰撞恢复系数,和投篮飞行轨迹无关。很多人把Bounciness调来调去找飞行问题,方向搞反了——飞行的曲线完全由Rigidbody的重力和初始速度决定,Bounciness只决定球碰到篮板后弹回来多少。真要调飞行手感,该改的是第3章脚本里的出手速度,而不是材质上的弹力。

2.3 计分检测:触发器与Tag的分工

得分检测我不用OnCollisionEnter。球碰到篮筐边缘的回弹是非持续接触,碰撞事件触发瞬间,且碰撞方向、力度会影响事件时序,很容易漏判。可靠做法是在篮筐内环放一个空物体,挂Box Collider,勾选Is Trigger,让球“穿过”它时触发得分。

写脚本前先把Tag做掉:在Inspector右上角Tags & Layers里新建一个Ball标签,拖到篮球Prefab的Tag栏。检测脚本放在触发器物体上,OnTriggerEnter里判断other.CompareTag("Ball")。为什么用Tag而不是直接GetComponent :球以后可能扩展成多种物体,Tag语义清晰,而且CompareTag不产生GC分配,移动端每帧触发时开销更低。

触发器的尺寸要手动收小。空物体默认Box Collider边长1米,直接挂在篮筐下会包住整个筐沿,导致球碰到筐外沿也算得分。我一般把Size的X和Z压到0.45,Y留0.1,让检测范围只覆盖圆环内口的85%左右,留一点体感宽容度。

2.4 UI反馈:数字先跑,再想动画

计分UI不要不停帧轮询,用事件驱动更干净。Hierarchy新建Canvas,加子物体Text取名ScoreText,Font Size给48,对齐到画面顶部居中。Canvas Scaler先随便选,到第5章再统一处理屏幕适配。

在HoopDetector里定义public event Action ScoreChanged;,进筐时score加1并通知。UI侧用一个ScoreUI脚本挂到Text上:

using UnityEngine; using UnityEngine.UI; public class ScoreUI : MonoBehaviour { private Text scoreText; void Start() { scoreText = GetComponent<Text>(); HoopDetector hoop = FindObjectOfType<HoopDetector>(); if (hoop != null) { hoop.ScoreChanged += OnScoreChanged; } } void OnScoreChanged(int newScore) { scoreText.text = "Score: " + newScore; } }

这里用事件的好处是UI脚本不用知道球在哪、物理怎么算,只要监听分数变化。调试阶段把Debug.Log放在HoopDetector里,事件链路测通了再删,后面接音效、接动画都只改监听方,不动检测逻辑。

3. 把投篮控制写成可复现的C#脚本:从拖拽到发射的完整路径

3.1 从屏幕拖拽到出手速度:Input.GetMouseButtonDown与Up

投篮输入是这个Demo的核心交互。最简单的实现是监听鼠标左键按下和抬起,用两点的屏幕坐标差值计算出手力度。脚本挂在空物体或主摄像机上,篮球Prefab通过Inspector拖进去:

using UnityEngine; public class BallShooter : MonoBehaviour { public GameObject ballPrefab; // 篮球预制体 public Transform spawnPoint; // 每次出手的球生成位置 public Transform hoopTarget; // 篮筐参考点,用于计算出手方向 public float powerScale = 0.04f; // 像素距离转力度系数 private Vector3 dragStartPos; // 按下时记录的屏幕坐标 private bool isDragging; // 是否处于拖拽状态 void Update() { // 桌面端鼠标;移动端触摸也会命中同一套值 if (Input.GetMouseButtonDown(0)) { dragStartPos = Input.mousePosition; isDragging = true; } // 松开鼠标即出手 if (Input.GetMouseButtonUp(0) && isDragging) { Vector3 dragDelta = Input.mousePosition - dragStartPos; // 约定只有向上滑才出手,避免误触 if (dragDelta.y > 20) { float power = Mathf.Clamp(dragDelta.y * powerScale, 6f, 18f); LaunchBall(power); } isDragging = false; } } void LaunchBall(float power) { GameObject ball = Instantiate(ballPrefab, spawnPoint.position, Quaternion.identity); Rigidbody rb = ball.GetComponent<Rigidbody>(); rb.velocity = ComputeLaunchVelocity(power); } Vector3 ComputeLaunchVelocity(float power) { // 从出手点指向篮筐的水平方向 Vector3 toHoop = (hoopTarget.position - spawnPoint.position); toHoop.y = 0f; toHoop.Normalize(); // 垂直分量加0.7,形成约55度的出手仰角 Vector3 dir = (toHoop + Vector3.up * 0.7f).normalized; return dir * power; } }

逻辑说明:Update里每一帧检测鼠标状态,按下时记录起始屏幕坐标,抬起时计算delta。delta.y大于20像素才触发投篮,避免玩家随手一碰就出手。ComputeLaunchVelocity里先去掉目标点的Y轴,只拿水平朝向,再叠加上0.7的垂直分量并归一化,这个比例得到的仰角接近真实投篮的出手角度。

参数说明:powerScale=0.04是1920x1080分辨率下的起点值,如果你在手机上测试,屏幕像素密度低,建议改成0.06。clamp到6到18是为了把力度压在一个物理上合理的区间,防止极端操作让球瞬间飞出场景。初始速度用velocity直接赋值,而不是AddForce,因为速度是确定性的,好调试,也不会因为物理帧波动产生额外误差。

3.2 移动端触摸适配:TouchPhase与多点触控的取舍

Input.GetMouseButton系列在移动端也能工作,Unity会自动把第一根手指映射成鼠标。但如果想在真机上做更精细的手势控制,或者防止第二根手指触碰屏幕时误触发投篮,就用Input.touches自己管理。我一般会用下面这段替换3.1的输入部分:

using UnityEngine; public class TouchShootInput : MonoBehaviour { private Vector2 touchStart; private BallShooter shooter; // 复用上一节的发射逻辑 void Awake() { shooter = GetComponent<BallShooter>(); } void Update() { if (Input.touchCount == 0) return; Touch t = Input.GetTouch(0); if (t.phase == TouchPhase.Began) { touchStart = t.position; } else if (t.phase == TouchPhase.Ended) { Vector2 delta = t.position - touchStart; if (delta.y > 20f) { // 换算后的出手力度范围与鼠标方案保持一致 float touchPower = Mathf.Clamp(delta.y * 0.06f, 6f, 18f); // 调用发射方法 } } } }

这段代码只监听touchCount==0时return,也就是第一根手指操作期间,第二根手指按下去不会触发任何逻辑。对篮球Demo来说,双指同时操作没有意义,反而容易误射。touchPower的0.06系数是移动端屏幕像素密度下的经验值,你和3.1的0.04对比就会发现,它专门为1080p上下的手机调过。

如果你不做移动端,鼠标方案已经够用;如果打算给朋友装到安卓手机上,这段触摸适配就省不掉。还有一种常见做法是把出手方向做成固定45度角,玩家只控制力度,那会牺牲操作感,不推荐。

3.3 进筐检测与得分事件:HoopDetector完整脚本

第2章讲过触发器的原理,这里给完整实现。比基础逻辑多一个判断:只认向下穿筐的球。因为球弹到筐底边缘再顶上来也会经过触发器,不判断方向就会乱加分:

using UnityEngine; using System; public class HoopDetector : MonoBehaviour { public event Action<int> ScoreChanged; private int score; void OnTriggerEnter(Collider other) { if (!other.CompareTag("Ball")) return; // 只有向下穿过篮圈才算得分,防止球从筐底顶上来误判 Rigidbody rb = other.GetComponent<Rigidbody>(); if (rb != null && rb.velocity.y < 0) { score++; ScoreChanged?.Invoke(score); Debug.Log("得分:" + score); } } }

挂在篮筐内环的空物体上,Collider的Is Trigger勾上,Tag预先在篮球Prefab上配好。velocity.y小于0这个条件很关键,如果去掉,球先弹到筐下沿再蹦进框,也会被当成得分,玩家会觉得计分完全不准。

事件ScoreChanged让UI层和逻辑层彻底分开。以后想加进筐音效、震动反馈,只需要在别处再订阅同一个事件,不需要回来改这个脚本。这也是我为什么反复强调Demo也要把事件写成public event而不是在脚本里直接FindObjectOfType去改UI文本——后面每多一个反馈,省的时间就多一分。

3.4 球的生命周期:投丢的球怎么回收

球投出去以后不会消失,必须处理三种情况:进球后球弹走、砸筐后滚落、投偏后飞出场地。最简单有效的办法是给篮球预制体挂一个自毁脚本:

using UnityEngine; public class BallRespawn : MonoBehaviour { public float fallThresholdY = -5f; // 低于此高度直接销毁 public float lifeTime = 3f; // 最长存活时间,防止球卡在角落 private float elapsed; void Update() { elapsed += Time.deltaTime; if (transform.position.y < fallThresholdY || elapsed > lifeTime) { Destroy(gameObject); } } }

fallThresholdY取-5,只要球掉到场地下方或者滚出场景边界就会被回收。lifeTime给3秒,防止球卡在篮筐和篮板缝隙里一直占用物理计算。生命周期这一步很多新手会漏掉,结果玩到第10球时场景里堆了20个刚体,手机瞬间掉帧。

如果想做得更工程化,可以用对象池替代Instantiate/Destroy,篮球反复用同一个实例。但这个Demo规模下一颗球来回收的Instantiate负数开销完全可以接受,真到要做多球同时在场的时候,再回来换成对象池不迟。提前优化不是好事,先跑通再优化才符合Demo的定位。

4. 手感翻车的高频原因:Unity3d物理参数与篮球Demo避坑

4.1 篮球高速穿筐:Collision Detection Mode为什么必须调

现象:球以较高速度飞向篮筐时,直接从圆环中间穿过去,或者穿过篮板掉到后面,完全无视碰撞。

原因:Unity的Rigidbody默认Collision Detection是Discrete,物理引擎只在每个物理帧结束时做一次位置检测。如果球在两帧之间移动的距离超过了碰撞体厚度,引擎会认为球直接跳过了碰撞体,这就是高速穿透。投篮的球速集中在10到18,恰好是穿透高发区间。

解决:选中篮球预制体的Rigidbody,把Collision Detection改成Continuous或Continuous Speculative。我一般用Continuous,它会让球和静态碰撞体做更精细的扫描检测,代价是额外CPU消耗。这个Demo里同时只有一到两颗球,开销完全可以忽略。如果你后面做弹幕类游戏,几十颗高速球同时飞,那就要重新掂量了。

4.2 投出去的球乱飞:Angular Drag和质心偏移的坑

现象:球飞行路线前半段正常,后半段明显偏转,甚至像自己拐了弯。

原因:球出手后处于高速旋转状态,Angular Drag默认是0.05,太低了,旋转动能衰减慢,球在飞行过程中受到的风阻效应就被放大,轨迹自然偏离。另一个隐藏坑是模型素材中心不在球心,Rigidbody的质心跟着偏,飞出去就像一边重一边轻。

解决:把Angular Drag提到0.1,给旋转额外阻力。如果改完还偏,就在球的子物体下挂一个空物体,在Rigidbody面板里手动设置Center of Mass,把它精确对齐到球体中心。这个操作在Inspector里可以可视化,把质心小球拖到球心位置即可。我碰到过三次这种乱飞问题,两次都是素材中心偏移,一次是Angular Drag太低,基本就这两个原因。

4.3 计分误判:触发器尺寸比视觉篮筐大一圈

现象:球明显碰到篮筐外沿却给分,或者球已经从筐里掉下去却不记分。

原因:装有HoopDetector的碰撞体自带默认尺寸,空物体上的Box Collider默认是边长1米的立方体,直接挂在篮筐模型下会把检测区域撑得比圆环大一圈。

解决:新建空物体后,手动把Box Collider的Size调整到0.4到0.45之间,Y轴给0.1,让触发器只包住篮筐内环的85%。运行时在Scene视图选中触发器,打开Inspector里Collider的Edit Bounds,直接拉线框看它和视觉圆环的贴合程度。计分宽容度这里要做批量测试,投20个球看判定是否稳定,再微调尺寸。

4.4 手感要么没力要么力大飞砖:力度映射不能直接用像素

现象:拖10像素球只飞半米,拖50像素直接飞到场景外,中间几乎没有线性过渡。

原因:物理引擎对初始速度的反应是平方级的,而屏幕像素距离是线性的,直接用delta.y乘一个固定系数,力量曲线会非常陡。玩家会感觉要么轻了要么重了,怎么滑都找不到中间值。

解决:把delta.y先做clamp再乘系数,把力度压在6到18的区间内。方向向量保持normalized,这样变化的只有标量力度,不会让角度也跟着拖拽距离变。手感调试有一个小技巧,就是先固定力度为10,把所有方向角度都试通,再把力度范围加回来,两步分开调比边投边猜快得多。

4.5 安卓真机掉帧发热:Fixed Timestep别乱调

现象:PC编辑器里60帧稳定,打包到安卓手机上只有30帧,机身后盖烫手。

原因:最常见的是把Physics里的Fixed Timestep从0.02调成了0.01,想“让物理更流畅”,结果物理运算频率翻倍,手机上CPU直接被物理线程吃满。另一种情况是场上球太多,每颗刚体都在参与碰撞求解。

解决:Fixed Timestep保持0.02不要动。球的数量限制在2颗以内,出手时如果上一颗球还没回收,先把旧的Destroy掉再Instantiate新的。真机性能验证用Profiler连接看Physics.Simulate的耗时,如果占比超过主线程20%,先砍球数,再检查有没有物体不该挂Collider却挂了。篮球和篮板这些必须碰撞,但地面Plane上的Collider可以保留,毕竟球要落地。

5. 从Demo到可玩:打包、真机适配与性能验证

5.1 安卓打包前Build Settings最低配置

Demo做到能跑还不够,最终要在手机上给朋友看才算完整。File > Build Settings切到Android,点击Switch Platform。Player Settings里主要动三处:Scripting Backend选IL2CPP,Target Architectures勾ARM64,Internet Access如果不需要就别勾。IL2CPP首次编译很慢,会卡在Building Library十几分钟,这是正常的,别以为死了。

顺便提醒一下,如果电脑没装Android SDK和JDK,Unity Hub会提示你安装,一路默认即可。打包出的APK如果装到旧手机上打不开,检查ARMv7是否也需要勾上。大部分2020年以后出的手机都支持ARM64,所以平时只勾ARM64就够了。

5.2 Canvas在手机上错位:CanvasScaler的Scale With Screen Size

现象:在1920x1080的Game预览窗口里UI位置正常,打包到带刘海屏的手机上按钮偏移、计分文字跑出屏幕。

原因:Canvas组件默认的Constant Pixel Size按物理像素渲染,屏幕分辨率一变,UI的实际大小和位置全部错位。

解决:选中Canvas,把Canvas Scaler的UI Scale Mode改为Scale With Screen Size,Reference Resolution写1920x1080,Screen Match Mode选Shrink。这样UI会根据实际分辨率等比缩放。刘海屏的适配要额外用Screen.safeArea把UI约束在安全区域内,ScoreText这种顶部元素尤其要注意。没有这个设置之前,我的Demo在iPhone上计分数字直接进了刘海区域,看起来非常业余。

5.3 Profiler看三组数据:Physics.Simulate、Scripts、PlayerLoop

真机性能验证别靠体感,用Profiler说话。Unity的Profiler窗口连上Development Build后,CPU Usage模块里只看三组数据。Physics.Simulate耗时高,说明物理物体太多或Collider过于复杂;Scripts耗时高,去查Update里有没有每帧Instantiate、GetComponent、FindObjectOfType;PlayerLoop偏高,多半是渲染压力,先看阴影质量。

我一次在安卓机上把Quality Settings里的阴影从Hard Shadows改成Soft Shadows,帧率立刻从38掉到26,改回去就好了。移动端优先关阴影、关HDR、降抗锯齿,这三个按钮对帧率的影响比任何代码优化都明显。Profiler里看到的数字才是评判标准,不要凭感觉调参数,那是玄学。

5.4 复现性:别人打开你工程时的三个文件层次

现象:把Assets文件夹压缩包发给朋友,对方用不同Unity版本打开,报错一堆脚本找不到。

原因:Unity工程的脚本引用存在场景文件里,但脚本本身和Meta文件里的GUID绑定,工程版本差异又会导致包的API不一致。只发Assets等于丢了半个工程。

解决:发整个项目文件夹,至少包含Assets、ProjectSettings和Packages三个目录。ProjectSettings里记录的是Unity版本、输入系统配置、质量设置,缺了它大概率打不开。用Unity Hub创建项目时如果版本不一致,让朋友先升级或降级到同一版本,比如2021.3 LTS,这是目前兼容性最好的长期支持版本。如果你用了新版Input System包,朋友没导入,Input.GetMouseButton全部失效,这种问题排查起来极其痛苦,发工程前先自己解压到干净目录试一次。

6. 让Demo看起来像样:投篮轨迹预览与手感可调参数

6.1 出手前画一条抛物线:LineRenderer的物理预测

投篮Demo最加分的一个小功能,是拖拽时实时显示球的预计飞行轨迹。实现思路不做真正的物理模拟,而是用同一套重力公式逐点累加,在很小误差下画出整条弧线。给球前方挂一个LineRenderer,然后用这段脚本实时更新轨迹点:

using UnityEngine; public class TrajectoryPreview : MonoBehaviour { public LineRenderer line; // 场景里的LineRenderer线 public int segments = 30; // 轨迹点数量 public float timeStep = 0.03f; // 每点之间模拟间隔 public void ShowPrediction(Vector3 startPos, Vector3 velocity) { Vector3 pos = startPos; Vector3 vel = velocity; line.positionCount = segments; for (int i = 0; i < segments; i++) { line.SetPosition(i, pos); vel += Physics.gravity * timeStep; pos += vel * timeStep; } } public void Clear() { line.positionCount = 0; } }

segments给30,timeStep给0.03,总预测时长约0.9秒,正好覆盖一次投篮的完整飞行时间。Physics.gravity默认是(0, -9.81, 0),和物理引擎实际使用的一致,所以预测轨迹和真实球路几乎重合。在拖拽过程中每帧调用ShowPrediction,传入spawnPoint和待发射的初始速度,松开手后调用Clear。

有一个验证预测是否准确的笨办法:在LaunchBall里把rb.velocity打印到Console,再对比ShowPrediction传进去的velocity,两者数值应该一致。误差超过0.001就去检查是不是把timeStep和FixedDeltaTime混用了。我做这个功能时第一版用的Time.fixedDeltaTime,结果预测轨迹比实际球路偏高,排查了很久才发现是步长取错,这类差错只能靠验证脚本抓。

6.2 参数外置:手感调整不用每次改代码

投篮力度、出手角度、球的弹力这些参数,不要写死在脚本里。把它们集中放到一个ScriptableObject或者一个公开的float字段面板上,调手感时直接在Inspector里拖,不用每次改代码重新编译。我自己的习惯是建一个BallSettings资产文件,里面存powerScale、clamp的最小最大力度、出手仰角系数、Bounciness这些值,然后让BallShooter和TrajectoryPreview都引用同一份配置。

这个习惯是被一次教训逼出来的。之前某个版本把力度系数写死在代码里,策划说“手感不对”要调,改一个数字编译一次,来回十几次才满意。参数外置之后,只需要在Inspector里拖动,观察球路变化,整个调试过程从十分钟缩短到两分钟。调试手感时先试三个极端值——最小力度、最大力度和中间力度,确认边界都合理,再让朋友盲测中段力度的手感范围,比一个人闷头调半天有效得多。希望这段经验能帮你把Demo做得更顺手,也希望你和我一样,把第一次投篮投进的那一瞬间当成做游戏最原始的快乐。

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

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

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

立即咨询