尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Unity保龄球物理模拟实战:从刚体参数到计分逻辑
简介一款面向Unity开发者和休闲游戏爱好者的高质量3D保龄球体育游戏完整源码。项目基于C#编写完整支持Android与iOS平台内置Unity广告横幅和插页式广告接入示例并具备离线运行、平板适配、一键开局等特性。压缩包共2001个文件大小约373.63MB除核心C#脚本外还包含FBX角色与球道模型、Prefab预制体、Material材质、Animation动画与Animator Controller控制器、Shader着色器等覆盖场景搭建、物理碰撞、保龄球出手手感、球瓶倒落判定和广告播报的完整实现。完整文档与全部图形资源均已提供目录结构清晰便于修改和重新设计也适合学习移动端体育游戏的广告变现与项目架构。目前已有99人学习下载无论想快速上架一款真实感保龄球手游还是深入研究Unity游戏源码都能从中获得可直接落地的参考方案。1. 为什么真正的保龄球体验是物理题而不是美术题在Unity里做一款能打的保龄球难点从来不是画十条球道、摆十根球瓶而是怎么让球的滚动轨迹、球瓶被击中时的姿态和真实球道对得上。很多新手把球设置成刚体给一个初速度球瓶用一组Collider拼起来结果球穿过瓶子、瓶子只在原地抖却倒不下去、旋转也没有横向分量玩两局就露馅。像Real Bowling Experience这类标题的Unity体育游戏项目源码拆开看核心价值就三块C#驱动的投掷交互、可复现的刚体/碰撞参数、以及被许多人忽略的计分逻辑。下面按物理落地、滑动投掷、规则引擎、场景优化四个环节把能直接抄进自己项目的脚本、参数和踩坑点铺开。适合拿Unity做体育类demo、要把手感调到能让别人试玩的人也适合想系统看C#如何写刚体、碰撞层和计分数组的程序员。2. 刚体参数与碰撞层别让保龄球变成一颗台球2.1 默认Rigidbody为什么会让球“发飘”拿起Unity默认的Sphere加Rigidbody直接丢出去会发现球在球道上滑出去很远才滚起来落地像乒乓球一样弹跳。原因有两个默认刚体的质心是几何中心而真实保龄球的质心偏低内部配重块在球心下方默认的线性阻力为0角阻力0.05球道摩擦全靠Physic Material里的动摩擦系数起作用而默认材质是“无摩擦”的。保龄球的真实物理里球和球道之间是滚动摩擦加滑动摩擦的混合球出手瞬间有一个明显的前滑段之后因为手指带出的旋转开始过渡为滚动。如果只做“手感正确”的demo不需要内置那么细的物理模型但必须把三个参数绑定到Rigidbody上质量控制在5到7kg之间真实比赛用球常见6.8kg也就是15磅关闭自动质心手动把centerOfMass下沉0.02到0.03米把碰撞检测模式从Discrete切到ContinuousDynamic防止球高速穿过球瓶。这正是大多数Unity保龄球demo不会去做的事它们用Discrete速度一快就隧道穿透它们不设质量球与球瓶质量几乎相同导致球被瓶子“怼”回来。物理手感不是调Force拉大拉小而是先把质量、质心、接触材质和碰撞检测模式这四个根参数对齐到真实对象上再来谈力量曲线。2.2 球道、球瓶、球之间的Layer与Physic Material分配碰撞层设计决定了“该碰的碰不该碰的不碰”。保龄球场景里至少有三类刚体加一个地面球的刚体、十个球瓶的刚体、球道地面和两侧沟槽。Layer Collision Matrix在Project Settings → Physics里配置常见做法是分四层Ball、Pin、LaneStatic、Gutter。规则只有三条Ball层和Pin层必须互相碰撞这是球瓶倒掉的唯一来源Ball层和LaneStatic层必须碰撞这是球滚动和减速的来源Ball层和Gutter层碰撞但Pin层和Gutter层是否碰撞看游戏定我一般让已经歪出球道的球瓶直接掉进沟里比强行拦住更自然。Physic Material也是同一个思路。球道的动摩擦系数控制在0.4到0.6之间球瓶底部和球道的接触推荐0.2到0.3太高球瓶像被胶水粘住太低一阵风就全倒高摩擦让球瓶站得更稳低摩擦让受力后滑得更远这之间找0.25左右。球本身的Bounciness必须设在0.05以下否则球在球道上的微小起伏会弹跳起来视觉上非常不真实。弹性接近0时滚动状态由角速度和摩擦力自然生成。为什么这层对“真保龄球体验”这么重要因为玩家判断“这球打得对不对”依赖的是球瓶被击倒的扩散方式和声音而不是球走多远。球瓶摩擦参数过高撞倒时瓶子会在原地打转过低瓶子会滑行出球道穿模到背景里。这层参数是物理反馈的主干不是调着玩的。2.3 最小可用的保龄球球体物理脚本using UnityEngine; [RequireComponent(typeof(Rigidbody), typeof(SphereCollider))] public class BowlingBallPhysics : MonoBehaviour { [Header(刚体基础参数)] public float ballMass 6.8f; // 15磅常见比赛球重量 public float linearDrag 0.04f; // 空气阻力只影响长距离减速 public float angularDrag 0.06f; // 角阻力影响旋转衰减 private Rigidbody rb; private void Awake() { rb GetComponentRigidbody(); // 质量必须设置到Rigidbody而不是SphereCollider或Transform rb.mass ballMass; rb.drag linearDrag; rb.angularDrag angularDrag; // 关闭自动质心将质心压低0.02m让球滚动更稳 rb.automaticCenterOfMass false; rb.centerOfMass new Vector3(0f, -0.02f, 0f); // 防止高速穿透球瓶 rb.collisionDetectionMode CollisionDetectionMode.ContinuousDynamic; // 球体碰撞器的材质零弹性、高动摩擦 SphereCollider col GetComponentSphereCollider(); PhysicMaterial mat new PhysicMaterial(BallSurface); mat.dynamicFriction 0.5f; mat.staticFriction 0.3f; mat.bounciness 0.02f; mat.frictionCombine PhysicMaterialCombine.Average; mat.bounceCombine PhysicMaterialCombine.Minimum; col.material mat; // 球体半径设为真实尺寸直径约21.6cm col.radius 0.108f; } }逻辑说明ballMass直接写到Rigidbody.mass上但物理引擎里碰撞响应的核心是“质量比”。球瓶质量约1.5kg球6.8kg质量比接近4.5比1这样球在撞击后不会被明显弹回。线性阻力只对长距离滑行有影响不要在球道上用增高drag来模拟摩擦球在球道上减速的真正来源是PhysicMaterial的dynamicFriction和滚动时的角阻力。参数说明angularDrag不要超过0.1否则旋转值会在1秒内掉到一半以下钩球的侧旋效果肉眼可见地消失bounciness设为0.02而不是0是因为完全为0时碰撞接触点在极少数情况下会触发浮点抖动保留0.02能保证稳定。ContinuousDynamic适合球的刚体球瓶数量多、速度低继续用Discrete即可性能更好。2.4 四个核心参数对照表参数推荐范围手感表现常见误区Rigidbody.mass5~7 kg球撞瓶后几乎不减速瓶飞散靠Transform.localScale放大球体质量却还是1Rigidbody.drag0.02~0.08长距离滑行平顺不会突然停用drag代替球道摩擦滚动距离畸短dynamicFriction0.4~0.6球在球道上逐渐减速并滚向瓶区设0.2以下时球滑行过长像冰壶bounciness0~0.05球不弹跳瓶撞击后不翻滚设0.3以上时球和瓶都跳起来变成台球这四组参数是这类标题项目里最影响“像不像真球”的部分。把表里任何一组往极端调手感立刻从保龄球变成台球或皮球。我一般会在场景里准备好一套默认材质存成.asset文件编辑器中调好后运行时加载避免每次生成球体时new一个PhysicMaterial导致材质泄漏。3. 滑动投掷与拖拽力量控制像拉弹弓一样瞄准3.1 交互模型选择在手机和PC上做保龄球投掷最直观的交互是把球往前“推”手指或鼠标从屏幕中下方开始往屏幕上方快速滑动松开瞬间球被丢出。这个交互和Unity里做一个滑动条本质上同一个手势区别在于Slider用Drag事件改value而这里要用拖拽手势换算成三维方向。另一种常见做法是“按住瞄准、放开击发”它在触屏上最顺手但不能模拟真实保龄球的甩手腕动作。如果追求“像真保龄球”推荐滑动模型因为它把轨迹方向和出手速度这两个最关键的变量交给了玩家的手势而不是按钮。底层依赖是UGUI的EventSystem。把实现拖拽接口的脚本挂在一个全屏透明Image上这样拖拽响应不依赖具体物体也不会被场景里的碰撞体干扰。如果项目用旧版Input Manager直接读Input.mousePosition也行但EventSystem的好处是同时兼容鼠标和触摸PointerEventData里提供了pressPosition和position不用手动处理触点ID省下一堆if。投掷方向换算要特别注意。屏幕方向是二维的手指往右滑对应世界哪个方向取决于球道朝向。不能直接把屏幕delta给Force要做一次旋转投影。常见写法是new Vector3(-delta.x, 0, -delta.y)负号取决于摄像机朝向和球道摆放方向。不同项目球道朝向不同这个映射可能是正负号的不同组合调试时用一个可见的方向箭头一帧一帧看比盲调快得多。3.2 用LineRenderer画一条“够用”的瞄准线滑动过程中没有反馈手感是断的。最轻量的做法是LineRenderer加简单的幂次曲线起点在球的位置终点在球前方按力量缩放的位置中间点稍微往下偏移视觉上形成一条弯曲的投掷弧线。真的要精确预测滚动轨迹得写一个带摩擦和角速度衰减的模拟器但作为预览线没必要。有个细节值得注意不要让瞄准线直接画到瓶区一旦上线玩家会以为箭头指哪里球就去哪里但出手后的旋转会让实际线路偏左或偏右。我一般让预览线长度是理论出手距离的60%只给方向感不给落点承诺。LineRenderer的alignment设为ViewZ材质用Sprites/Default避免写自定义Shader。3.3 C#实现拖拽事件与出手力量映射using UnityEngine; using UnityEngine.EventSystems; public class SwipeThrowHandler : MonoBehaviour, IDragHandler, IEndDragHandler { [Header(引用)] public Transform ballTransform; public LineRenderer aimLine; public float powerFactor 0.12f; // 屏幕像素距离到冲量的换算系数 public float minPower 4f; public float maxPower 18f; private Vector2 pressPos; private Vector2 currentPos; private Rigidbody ballRb; private bool ballThrown; private void Start() { ballRb ballTransform.GetComponentRigidbody(); aimLine.enabled false; } public void OnDrag(PointerEventData eventData) { if (ballThrown) return; // 记录按下位置和当前位置每次拖拽都从起点重新计算 pressPos eventData.pressPosition; currentPos eventData.position; Vector2 delta currentPos - pressPos; // 忽略过短拖拽防止误触发 if (delta.magnitude 20f) return; // 将屏幕delta映射为球道平面上的方向 Vector3 throwDir new Vector3(-delta.x, 0f, -delta.y).normalized; float power Mathf.Clamp(delta.magnitude * powerFactor, minPower, maxPower); DrawPreview(throwDir, power); } public void OnEndDrag(PointerEventData eventData) { if (ballThrown) return; Vector2 delta eventData.position - eventData.pressPosition; if (delta.magnitude 20f) { aimLine.enabled false; return; } Vector3 throwDir new Vector3(-delta.x, 0f, -delta.y).normalized; float power Mathf.Clamp(delta.magnitude * powerFactor, minPower, maxPower); ballRb.AddForce(throwDir * power, ForceMode.Impulse); // 给一个与力量正相关的横向角速度让球产生滚动效果 float spinScale 2f power * 0.3f; ballRb.AddTorque(transform.right * spinScale, ForceMode.Impulse); ballThrown true; aimLine.enabled false; } private void DrawPreview(Vector3 dir, float power) { aimLine.enabled true; Vector3[] points new Vector3[3]; points[0] ballTransform.position; points[1] ballTransform.position dir * (power * 0.4f); points[2] ballTransform.position dir * (power * 1.2f); aimLine.positionCount 3; aimLine.SetPositions(points); } }逻辑说明IDragHandler在手指移动时反复调用OnEndDrag在松开时调用一次。pressPosition始终是按下那一下的坐标这样无论手指怎么抖动都以起点和终点的差决定方向和力量避免中途改了方向造成的控制漂移。ForceMode.Impulse直接修改速度而不是逐步加速适合一瞬间出手的投掷动作。AddTorque使用transform.right是因为假设球朝Z轴滚动角速度方向指向右侧如果球道方向不同这行要换成对应轴。参数说明powerFactor为0.12时每100像素约给12个单位冲量按球质量6.8kg计算maxPower为18时初速度约2.6m/s刚好能让球在20米球道上减速到瓶区。想更顺手一点就加到0.15但注意球速过快时ContinuousDynamic也挡不住穿透需要配合球瓶的碰撞体厚度一起调。minPower设成4防止拖了很短就丢出去球软绵绵滚到一半停住。3.4 出手手感参数速查参数推荐范围手感效果调试技巧powerFactor0.08~0.16拖拽长度与球速的比例感固定10cm拖拽线观察球速曲线minPower / maxPower4 / 18决定出手下限不强、上限不失控先把上限调到28感受极端再退回18横向灵敏度0.8~1.2影响钩球左右偏差用同一方向连打十局看球瓶散点spinScale1.5~4角速度越大球越钩侧旋越明显从0开始加观察12号瓶位的散开角度4. 计分规则与球瓶重置玩得起一局的C#逻辑4.1 最容易写错的Strike和Spare计分保龄球按局计分一局10轮每轮最多投两球。第一球全倒叫Strike本轮结束两球全倒叫Spare本轮结束。最容易出错的是“加击”Strike本轮记10分再加后续两球的实际分数所以第10轮如果投了Strike必须多投两球让分数能结算Spare本轮记10分再加下一轮第一球分数所以第10轮有Spare时要补投一球。用C#数组实现时用int[,]固定尺寸比ListListint更合适尺寸固定到10轮加2个补投槽GC压力小。程序员接入C#数组时最容易踩的坑是第10轮出现Strike时后续两球没有归属轮次必须单独用“补投”槽位存不能直接往第10轮的第2个位置塞。得分计算不是实时往前加而是在每轮结束后“回头补分”。第1轮Strike后马上记不了第1轮的最终分数要等第2轮、第3轮的球打完才知道。这个特性决定了计分器的实现思路是“先存原始倒瓶数再一次性扫描结算”而不是打一球加一球。4.2 纯C#计分器用游标扫描替代逐轮累加public class BowlingScoreKeeper { public const int MaxFrames 10; private int[] rolls; // 展开为单个投球序列长度最多21 private int rollIndex; // 当前要写入的投球槽 public BowlingScoreKeeper() { rolls new int[21]; rollIndex 0; } // 记录一球倒下的瓶数 public void Roll(int pins) { rolls[rollIndex] pins; } // 返回第frameId轮1起算的累计分数 public int GetScore(int frameId) { int score 0; int rollCursor 0; for (int f 1; f frameId; f) { if (IsStrike(rollCursor)) { // Strike本轮10 后两球实际倒瓶数 score 10 rolls[rollCursor 1] rolls[rollCursor 2]; rollCursor 1; // Strike只用掉一个槽位 } else if (IsSpare(rollCursor)) { score 10 rolls[rollCursor 2]; rollCursor 2; } else { score rolls[rollCursor] rolls[rollCursor 1]; rollCursor 2; } } return score; } public bool IsCurrentFrameStrike(int frameId) { int cursor 0; for (int f 1; f frameId; f) { if (rolls[cursor] 10) cursor 1; else cursor 2; } return rolls[cursor] 10; } private bool IsStrike(int idx) rolls[idx] 10; private bool IsSpare(int idx) idx 1 rolls.Length rolls[idx] rolls[idx 1] 10; }逻辑说明rolls数组把10轮、每轮最多两个子投球展开成一维数据流。Strike时本轮只消耗1个槽位后续“加击”是往后面的槽位里继续填GetScore扫描到Strike时固定往后多取两个rolls值。Spare则取下一轮第一个rolls值。这套游标扫描的实现在Unity游戏里很合适每帧查分数只有O(n)的扫描不产生GC分配也不依赖外部状态。参数说明Roll(int pins)的pins由场景中的球瓶倒下数量检测驱动。常见做法是每个球瓶挂一个检测脚本在碰撞回调里按角度判断是否倒平把倒掉的数量计数后调scoreKeeper.Roll。保龄球规则只关心“这一球倒了几根”不关心具体哪根所以不用给每个球瓶做唯一编号判定只有是否倒在球道内决定它是空瓶还是沟球。4.3 球瓶重置别用Instantiate复用数组每次投球结束场景要做三件事等球和瓶完全停稳最简单是等所有刚体的velocity.magnitude小于0.1并持续0.5秒结算本轮分数把倒下的球瓶状态标记等下一轮开始前重置。重置不要用Destroy加Instantiate频繁实例化球瓶预制体在移动端容易卡顿。正确做法是维护一个球瓶数组重置时把每个瓶的position和rotation恢复到初值velocity清零Collider和Rigidbody重新启用。这样做的另一个好处是如果后续要做慢动作回放可以直接对每个瓶缓存上一帧的Transform不需要额外录制Mesh快照。球瓶立着的判定用球瓶模型底部的transform.up与全局Vector3.up做点积点积小于0.6大约53度角就认为已倒。这比直接用velocity判断可靠因为球瓶在受击瞬间有速度却还立着。如果只数OnCollisionEnter的触发次数瓶子被碰到、抖了一下又立起来分数就已经多记了。4.4 计数参数与球瓶属性表参数推荐值作用注意MaxFrames10一局完整轮数第10轮单独处理Strike补投rolls数组长度21覆盖9轮正常加第10轮Strike补2球再长浪费再短越界倒伏角度阈值53度点积0.6判定倒伏的临界滚动的小滚轮不算倒球瓶质量1.4~1.6 kg撞击后散开更真实比球轻太多会像纸片飞底部接触材质dynamicFriction 0.25受击后滑行距离适中0.15以下滑出球道5. 摄像机跟随、阴影与VR扩展把画面调出“真”的感觉5.1 摄像机用SmoothDamp跟随不要Lerp到球体保龄球的摄像机有三种常见方案固定机位、跟随球位、投掷后切到瓶区视角。第三种最“真”但也是返工重灾区。直接把摄像机位置等于球位置加偏移球一加速画面就剧烈抖动。正确做法是Vector3.SmoothDamp做滞后跟随焦点略微偏向瓶区让玩家提前看到球道走向。using UnityEngine; public class CameraFollowBall : MonoBehaviour { public Transform target; public Vector3 offset new Vector3(0, 3.2f, -5.5f); public float smoothTime 0.18f; private Vector3 velocity Vector3.zero; void LateUpdate() { if (target null) return; Vector3 desiredPos target.position offset; // SmoothDamp的第三参数是ref它保存内部速度状态 transform.position Vector3.SmoothDamp(transform.position, desiredPos, ref velocity, smoothTime); // 焦点方向偏向球前方4米提前看见球道 Vector3 lookTarget target.position target.forward * 4f; transform.LookAt(lookTarget); } }smoothTime越大相机越拖。超过0.3会让人晕。跟随逻辑放在LateUpdate而不是Update物理在FixedUpdate更新如果跟随太早拿到的是上一帧的位置画面会抖。5.2 处理阴影黑斑和WebGL存档失败球道是大面积平面最常遇到的是Unity阴影问题Shadow Distance太近会导致球在球道远端突然没影子像飘起来。把Project Settings里的Quality中Shadow Distance调到80到100米Resolution改为Medium。方向光与平面夹角过陡时会出现阴影黑斑增大Shadow Bias到0.05以上解决。如果要把这个项目发到WebGL会遇到Unity WebGL使用IDBFS写入失败的错误。这是Unity WebGL的同步文件API被浏览器拒绝。改成PlayerPrefsUnity内部会落到IndexedDB。要保存自定义数据结构自己拼字符串用PlayerPrefs存比在WebGL上折腾File API更稳。5.3 扩展到Pico4时改什么同一套物理代码在Pico4或Quest上跑交互层换成XR控制器按下扳机、往前甩腕、松开控制器的速度向量同步给AddForce。把投掷输入抽象成IThrowInput接口PC用鼠标滑动实现VR用手柄速度实现物理和计分部分一行不改。如果后续要让项目支持Unity MR切换VR只需照这个思路再补一个XR输入适配器。这也是这个项目留一套源码最值的地方真正可复用的不是球瓶模型而是C#逻辑层与输入层的解耦。本文还有配套的精品资源点击获取
RELATED

相关推荐

矩形波导TE01与TM11模的时域动态仿真方法

矩形波导TE01与TM11模的时域动态仿真方法

简介:本资源是一份面向电磁场与微波技术初学者及MATLAB仿真实践者的教学型项目,聚焦矩形波导中TE01模与TM11模的电磁场传播特性可视化分析,解决理论抽象难理解、模式场分布缺乏动态直观呈现的问题。压缩包共6个文件,含4个核心MATL…

📅 2026/9/15 2:59:04
H-ui前端与Flask后端接口契约规范指南

H-ui前端与Flask后端接口契约规范指南

简介:本资源是一个基于H-ui前端框架与Flask后端的全栈学习实践项目,面向Web开发初学者及前后端协同开发入门者,重点解决UI组件集成、表单交互实现与前后端数据通信等典型问题。压缩包共1077个文件,涵盖359个HTML页面(含…

📅 2026/9/15 2:54:04
CS、CG、SF小信号建模实战:工艺感知型放大器设计指南

CS、CG、SF小信号建模实战:工艺感知型放大器设计指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/15 2:54:04
MORE NEWS

更多资讯

📰

10个静态页面搭建中华历史专题站:HTML5骨架+CSS变量+原生JS

简介:一套以中华历史为主题的HTML5CSS3JavaScript多页面网页设计成品,共10个独立页面,适合大学生完成Web前端期末作业、课程设计或毕设参考,也方便前端初学者通过完整项目学习网页制作流程。压缩包7.33MB,共86个文件&a…

📰

AI科研助手Skills实战:GitHub十大项目选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

新能源功率预测并网考核:实发功率与可发/可用功率口径打通实战

1. 从“实发功率”到“可用/可发功率”:并网考核的新痛点在风电光伏场站干过功率预测或者并网运行管理的人,应该都有同感:以前考核粗的时候,大家盯的就是一个“实发功率”,就是计量关口表上那个数,实际发了…

📰

Unity保龄球物理模拟实战:从刚体参数到计分逻辑

简介:一款面向Unity开发者和休闲游戏爱好者的高质量3D保龄球体育游戏完整源码。项目基于C#编写,完整支持Android与iOS平台,内置Unity广告横幅和插页式广告接入示例,并具备离线运行、平板适配、一键开局等特性。压缩包共2001个文件…

📰

矩形波导TE01与TM11模的时域动态仿真方法

简介:本资源是一份面向电磁场与微波技术初学者及MATLAB仿真实践者的教学型项目,聚焦矩形波导中TE01模与TM11模的电磁场传播特性可视化分析,解决理论抽象难理解、模式场分布缺乏动态直观呈现的问题。压缩包共6个文件,含4个核心MATL…

📰

H-ui前端与Flask后端接口契约规范指南

简介:本资源是一个基于H-ui前端框架与Flask后端的全栈学习实践项目,面向Web开发初学者及前后端协同开发入门者,重点解决UI组件集成、表单交互实现与前后端数据通信等典型问题。压缩包共1077个文件,涵盖359个HTML页面(含…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬