在Pong中,当球从球拍反弹时,如何计算球的方向?


28

我正在努力解决游戏开发中的这个Hello World-y问题。我已经在XNA中创建了一个TicTacToe游戏,所以我想下一步将是Breakout克隆。

请记住,我没有在游戏编程甚至什么数学,我应该适用于那些知识。这就是为什么我问这个问题。


问题:如何确定球击中屏幕底部的桨时应该反弹的位置?

我以为会是这样的:

  1. 捕获传入球的速度和角度。
  2. 检测它触摸条的位置(最左边,最右边,中间),并据此使其在触摸外部区域时具有更高的速度。
  3. 这就是我卡住的地方。呵呵。

有什么见解吗?我意识到这不是一个简单的答案类型问题,但是我确信这是每个​​人在某个时候都面临的问题。

我正在阅读该网站上推荐的《线性代数》一书,但我仍然不知道是否应该在这里应用它。


在突破前写出pong,然后可以导出球,墙和桨类,并对其进行扩展,以便它们与不同类型的积木和道具配合使用。另外,我认为乒乓比突破更简单。
隐身

Answers:


30

这是我在首页上的乒乓球上使用的相关逻辑:(请在阅读前先玩一下,以便您了解使用以下代码实现的效果)

本质上,当球与桨碰撞时,其方向完全被忽略。根据其与桨叶中心碰撞的距离,给出一个新的方向。如果球正好击中了球拍的中央,则将球精确水平地送出;如果碰到边缘,它将以极高的角度(75度)飞行。它始终以恒定的速度运行。

var relativeIntersectY = (paddle1Y+(PADDLEHEIGHT/2)) - intersectY;

取球拍的中间Y值,减去球的Y交点。如果桨高10像素,则此数字将在-5到5之间。我称其为“相对相交”,因为它现在位于“桨空间”中,即球相对于桨中间的交点。

var normalizedRelativeIntersectionY = (relativeIntersectY/(PADDLEHEIGHT/2));
var bounceAngle = normalizedRelativeIntersectionY * MAXBOUNCEANGLE;

取相对交点,将其除以桨高的一半。现在我们的-5到5的数字是从-1到1的小数;它已标准化。然后将其乘以您希望球反弹的最大角度。我将其设置为5 * Pi / 12弧度(75度)。

ballVx = BALLSPEED*Math.cos(bounceAngle);
ballVy = BALLSPEED*-Math.sin(bounceAngle);

最后,使用简单的三角函数计算新的球速度。

这可能不是您想要的效果,或者您可能还想通过将标准化相对交点乘以最大速度来确定速度。如果击中球拍边缘附近的球,则使球走得更快,如果击中中心球,则使球走得慢。


我可能想要一些关于向量的外观或如何保存球所具有的向量变量(速度和方向)的代码。

向量隐含包含速度和方向。我将向量存储为“ vx”和“ vy”;即,在x方向上的速度和在y方向上的速度。如果您还没有参加物理入门课程,那么这对您来说似乎有些陌生。

我这样做的原因是因为它减少了必要的每帧计算。每一帧,你只是做x += vx * time;y += vy * time;,时间是从最后一帧的时间,以毫秒为单位(因此速度是每毫秒像素)。


关于实现弯曲球的能力:

首先,您需要知道击球时桨的速度;这意味着您需要跟踪球拍的历史记录,以便知道球拍的一个或多个过去位置,以便可以将其与球拍的当前位置进行比较,以查看其是否移动。(位置变化/时间变化=速度;因此您需要2个或更多位置,以及这些位置的时间)

现在,您还需要跟踪球的角速度,该角速度实际上代表了球的行进曲线,但等效于球的真实旋转。类似于从与球拍碰撞时球的相对位置内插反弹角度的方法,您还需要从碰撞球拍的速度内插此角速度(或旋转角)。与其像弹跳角那样简单地设置旋转角度,不如您想在球的现有旋转角度上增加或减少,因为这在游戏中通常会很好地起作用(玩家可以注意到球在旋转,并使球旋转)甚至更加疯狂,或者与旋转相反以试图使其笔直移动)。

但是请注意,虽然这是最常识,并且可能是实现它的最简单方法,但是反弹的实际物理过程并不仅仅取决于其撞击物体的速度。没有角速度(无自旋)以某个角度撞击表面的对象将具有自旋。这可能会导致更好的游戏机制,所以您可能需要研究一下,但是我不确定其背后的物理原理,因此我不会尝试对其进行解释。


那就是我要的效果;碰到条形边缘的速度更快。多谢您抽出宝贵的时间写出来。我在理解一些事情时遇到了一些麻烦;例如,在第一个片段中,“ intersectY”是什么?另外,'paddle1Y'的高度是否正确?

intersectY是球与球拍相交的位置。我做了一个过于复杂的计算,老实说,我现在甚至还不明白,但是从本质上讲,它是球碰撞时的Y值。paddle1Y是从屏幕顶部开始的桨的Y值;PADDLEHEIGHT是桨的高度(“条”)。
莉吉特

为了允许“曲线”球,您必须添加什么?例如,当球即将击中桨叶时,您可以移动桨叶以使球弯曲。像这样:en.wikipedia.org/wiki/Curve_ball
Zolomon

查看编辑,让我知道您的想法(如果您需要更多有关某事的信息,不要得到某事,等等)
Ricket 2010年

谢谢!极好的答案,我一直想知道如何做到这一点!
Zolomon

8

自从我这样做已经有一段时间了,但是我认为我做对了。

给定完美的碰撞,反射角等于入射角。

您知道球拍的法线(假设是平坦的表面):N您知道球的原始位置(在时间步的开头):P您知道球的新位置(在时间步的结尾): P'您知道您的碰撞点:C假设您计算出段P-> P'通过桨板,则新的反射位置(P'')为:

P'+ 2 *(N *(P'点-N))

N *(P'点-N)子表达式计算沿球行进的碰撞法线的深度。减号是为了纠正我们正在检查与法线方向相反的深度的事实。

P'+ 2 *子表达式的一部分将球移回碰撞平面上方,深度为碰撞深度的2倍。

如果您不希望发生完美的碰撞,请将系数2更改为(1 +(1-k)),其中k是您的摩擦系数。完美碰撞的ak值为0,从而导致反射角与入射角完全相同。k值为1会引起碰撞,球会停留在碰撞平面的表面。

新的速度矢量V''的方向将为P''-C.对其进行归一化并乘以传入的速度,您得到的速度幅值将相同,但方向是新的。您可以通过乘以系数l来获得该速度,该系数将增大(l> 1)或减小(l <1)所得的速度。

总结一下:

P''= P'+(1-k)*(N *(P点-N))V''= l * V *((P''-C)/ | P''-C |)

其中k和l是您选择的系数。


5

可以“正确”完成反射,也可以“轻松”完成反射。

“正确”的方法是计算垂直于墙的向量。在2D中,这非常容易,您可能只需对其进行硬编码。然后,反射步骤实质上使运动的“平行”分量完整无缺,并使“垂直”分量反转。网上可能有关于此的详细信息,甚至在MathWorld中也是如此。

“简单”的方法是在碰壁时取消X或Y运动。如果击中侧壁,则将否定X。如果击中顶部,则将否定Y。如果您想加快球速,则只需增加所需的值即可。您可以通过将X和Y速度相乘来使其沿当前方向加速,或者只能在一个轴上加速。


上面描述的“简单”方式和“正确”方式不是基本相同吗?
汤姆

如果壁沿主轴,则它们完全相同。如果壁不是全部沿X,Y和Z轴排列,则不是,则两者完全不同。
dash-tom-bang

5

我也自己制作了类似打砖块的游戏,我认为解决球击球时的行为方式比采用sin / cos方法更简单,更快捷……对于a像这样的游戏。这是我的工作:

  • 当然,由于球速度随时间增加,因此我在x,y步骤之前/之后进行插值以保持精确的碰撞检测,因此遍历所有“ stepX”和“ stepY”,这是通过将每个速度分量除以所形成的矢量的模数而得出的通过当前和未来的球位置。

  • 如果发生与桨的碰撞,我将Y速度除以20。这个“ 20”是最方便的值,当球击到桨的侧面时,我会得到最大的角度,但是您可以将其更改为任意值您的需求是,只要发挥一些价值观,然后为您选择更好的。假设速度为5(这是我的初始游戏速度)除以该数字(20),得出的“反弹系数”为0.25。当速度随时间增加到最大速度值(例如可能为15)(在这种情况下:15/20 = 0.75)时,此计算使我的角度保持相当比例。考虑到我的球拍的x,y坐标是中间的(x和y代表球拍的中心),因此我将这个结果乘以球位置和球拍位置之间的差。差异越大,大耳所产生的角度。此外,使用中位桨叶,您可以根据球的击打面获得x增量的正确符号,而不必担心计算中心。用伪代码:

对于n = 0模...

如果碰撞检测到,则speedX =-(speedY / 20)*(paddleX-ballX); speedY = -speedY;
出口; 万一

...

x = x + stepX; y = y + stepY;

结束于

记住,请始终保持简单。希望对您有所帮助!


4

当Breakbreak中的桨叶遵循您描述的样式时,通常将其建模为曲面。入射角根据其击中桨叶的位置而变化。在死点处,曲线的切线绝对是水平的,并且球像预期的那样反射。当您偏离中心时,曲线的切线将变得越来越成角度,因此球的反射也将有所不同。

关键是改变的是反射角而不是球的速度。随着时间的推移,球速通常只会缓慢增加。


1
您说的是曲面,这在我脑海中听起来很合逻辑,但是一旦我尝试从代码上考虑它,事情就会很快变得模糊。我什至不能在代码中将其声明为变量或其他内容。

诸如此类angle = 1 - 2 * (ball.x - paddle.left) / paddle.width的数字将为您提供1到-1之间的数字;(有时是为游戏机制调整的一些值)是切线在球碰撞点处的斜率。反映出那条线而不是严格的水平线。

4

周末,诺兰·布什内尔Nolan Bushnell)在SIEGE作了主题演讲,并谈到了与原版乒乓球类似的问题。您不必进行很多复杂的计算。如果您朝着面板左侧击球,则将球向左发送。对右侧执行相同的操作。

首先,您可以将左右两侧的角度设置为45度。一旦您完成了游戏,就可以返回并使其更加复杂,但是从一开始就使其尽可能简单。


1
我也看到了该主题演讲,他的观点是这是一个设计决策,而不是数学决策:“入射角=反射角”将是正确的,但会导致较弱的游戏玩法。此外,在最初的Pong and Breakout中,速度是球/桨碰撞次数的函数(因此,速度会随着时间而加快)。在一定次数的击打之后,它们还减小了桨叶的尺寸。我会避免让球直线上升,然后将球拍无限期地留在那里。
伊恩·施雷伯

4

Breakout是一门经典的初学者作品,它开始涉足基于物理的游戏编程世界。基本上,当球击中墙壁时,它会有弹跳动作。正如上面的建议,入射角等于反射角。但是,当您考虑将球击中桨时。逻辑分为3个部分。1.)球击中桨的中心部分。2.)球击中了桨的左侧。3.)球击中了桨的右侧位置。

当您考虑中心部分时:击球时,您无需改变弹跳效果。球只是正常偏转,但是当击中任一方向时,情况就不同了。

当球被击向左侧时,即认为球来自屏幕的左侧,而您的球拍则来自右侧。然后,当您向左击球时,球应以相同的角度反射到来自.ie的方向。反之亦然。在右边部分也适用相同的内容。

当球被击中时,球向左或向右的运动使其更可信。

希望你有这个主意,至少在逻辑上是明智的。谢谢


1

想象一下,您计算了桨中心与球Y击中的点之间的距离,并将其称为d。假设d当球击中球拍中心上方时,它具有正值。现在,您可以增加d * -0.1球的Y速度,它将开始改变方向。这是javascript中的示例,可以轻松将其转换为c#!

var canvas = document.querySelector('canvas');
var resize = function () {
  canvas.width = innerWidth;
  canvas.height = innerHeight;
};
resize();
var ctx = canvas.getContext('2d');
var ball = {
  size: 3,
  x: 1,
  y: canvas.height/2,
  vx: 2,
  vy: 0
};
var paddle = {
  height: 40,
  width: 3,
  x: canvas.width/2,
  y: canvas.height/2
};
addEventListener('mousemove', function (e) {
  paddle.y = e.clientY - (paddle.height/2);
});
var loop = function () {
  resize();
  ball.x += ball.vx;
  ball.y += ball.vy;
  if (ball.x > canvas.width || ball.x < 0) ball.vx *= -1; // horiz wall hit
  if (ball.y > canvas.height || ball.y < 0) ball.vy *= -1; // vert wall hit
  if (ball.x >= paddle.x && ball.x <= paddle.x + paddle.width && ball.y >= paddle.y && ball.y <= paddle.y + paddle.height) {
    // paddle hit
    var paddleCenter = paddle.y + (paddle.height/2);
    var d = paddleCenter - ball.y;
    ball.vy += d * -0.1; // here's the trick
    ball.vx *= -1;
  }
  ctx.fillRect(ball.x,ball.y,ball.size,ball.size);
  ctx.fillRect(paddle.x,paddle.y,paddle.width,paddle.height);
  requestAnimationFrame(loop);
};
loop();
body {overflow: hidden; margin: 0}
canvas {width: 100vw; height: 100vh}
<canvas></canvas>



0

嗨,我最近尝试制作一个球类游戏,并为此找到了解决方案。所以我做了什么:在玩游戏时,桨在运动。我的坐标系保持不变,画布的左上角点是0,0。桨在此坐标系中移动。x轴从0指向画布宽度,y轴从0指向画布高度。我创建了一个固定大小为100的宽度和20的高度的桨。然后我在其周围绘制一个想象的圆圈。当球击中桨时,我会计算桨的中心点

double paddleCenter=Squash.paddle.getXpos()+Squash.paddle.getPaddleWidth()/2;

然后我从球的当前位置减去中心,这样坐标系将位于球拍的中心,ballCenter是球击中球拍的点(-(paddlewidth + r).. 0 ..(paddlewidth + r ))这不过是重新调整桨的击球点

double x0 = ballCenterX-paddleCenter;

借助球的击球点(x0)计算圆的相交点,这是一个重新计算,我们用已知的x0坐标求圆上的y坐标,并且需要翻转y轴

double y0 = -Math.sqrt(paddleRadius*paddleRadius-x0*x0);

计算半径为paddle的圆的正态方程的导数,圆的半径定义为paddleRadius f(x,y)= x ^ 2 + y ^ 2-r ^ 2

double normalX=2*x0;
double normalY=2*y0;

归一化N向量,以获得表面法线的单位向量

double normalizer=Math.sqrt(normalX*normalX + normalY*normalY);
normalX=normalX/normalizer;
normalY=normalY/normalizer;

现在我们有了桨的归一化(单位)表面法线。使用这些表面法线计算新方向,这将借助反射向量公式进行计算:new_direction = old_direction-2 * dot(N,old_direction)* N,但是如果表面法线始终指向上方,则法线将球击中桨的位置不断变化

double eta=2; //this is the constant which gives now perfect reflection but with different normal vectors, for now this set to 2, to give perfect reflection
double dotprod=vX*normalX+vY*normalY;
vX=vX-eta*dotprod*normalX;//compute the reflection and get the new direction on the x direction
vY=-vY;//y direction is remain the same (but inverted), as we just want to have a change in the x direction

我已经发布了针对该问题的解决方案。有关更多详细信息和完整游戏,请参见我的github存储库:

https://github.com/zoli333/BricksGame

用eclipse用Java编写。Ball.java中对此有另一种解决方案,其中不会发生重新缩放,我不将坐标坐标系移动到桨的中心点,而是从相对于左上角的0,0坐标系计算以上所有坐标桨的中心点。这也可以。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.