paddle bounce
continuous-integration/drone/push Build is passing Details

main
Evan Hemsley 2020-07-17 22:53:11 -07:00
parent d4459b9b3c
commit 5ee392b254
1 changed files with 239 additions and 116 deletions

View File

@ -10,137 +10,260 @@ If we look at the original Pong, we see that the angle of the ball is actually a
This is a classic risk/reward mechanic: it's safer to hit the center of the paddle, but it's easier for your opponent to return the shot. It's riskier to hit the edges of the paddle, but the shot is more difficult to return.
Let's return to our BallPaddleCollisionEngine.
We could modify our BounceEngine to handle angled bounces... but hang on a sec. Doesn't our boundary system use the bounce behavior? We don't want angled bounces when the ball bounces off the edges of the play area. It actually sounds to me like this is a new Actor collision behavior.
```ts
const velocity_message = this.emit_component_message(UpdateVelocityMessage, ball_velocity);
velocity_message.x_delta = 2 * message.normal.x * Math.abs(ball_velocity.x);
velocity_message.y_delta = 2 * message.normal.y * Math.abs(ball_velocity.y);
```
Create a file, **PongFE/Components/CanCausedAngledBounceComponent.cs**:
This is what is reflecting our ball's velocity.
```cs
using Encompass;
Let's calculate a new velocity based on where the ball touches the paddle.
First, we don't want the speed of the ball to change. So let's store it. The way we calculate speed is by obtaining the length of the velocity. We also want to know if the ball was travelling right or left at the time of contact.
```ts
const speed = len(ball_velocity.x, ball_velocity.y);
const horizontal = ball_velocity.x < 0 ? 1 : -1;
```
Now we want to calculate our new angle. First, we want to check how far the point of contact was from the center of the paddle. The problem is that we don't know inside our engine how big the paddle is. Let's fix that.
Let's return to our PaddleMoveSpeedComponent.
```ts
import { Component } from "encompass-ecs";
export class PaddleMoveSpeedComponent extends Component {
public y: number;
namespace PongFE.Components
{
public struct CanCauseAngledBounceComponent : IComponent { }
}
```
I think we should just store all of our paddle-related information here, so let's rename it to PaddleComponent, and rename _y_ to *move_speed*. Make sure to use VSCode's rename feature so you don't have to manually tweak the names in multiple files.
And another new file, **AngledBounceMessage.cs**:
```ts
import { Component } from "encompass-ecs";
```cs
using Encompass;
using PongFE.Enums;
export class PaddleComponent extends Component {
public height: number;
public move_speed: number;
}
```
namespace PongFE.Messages
{
public struct AngledBounceMessage : IMessage
{
public Entity Bounced { get; }
public Entity Bouncer { get; }
public HitOrientation HitOrientation { get; }
Let's change our PaddleSpawner to comply with our changed component.
```ts
const paddle_component = paddle_entity.add_component(PaddleComponent);
paddle_component.move_speed = message.move_speed;
paddle_component.height = height;
```
Now, back in our collision engine, we can get a new rotation. First, we figure out how far the contact was from the center of the paddle. Then we convert that to a number ranging from -1 to 1. Finally, we multiply that number by pi/4 to get a number ranging from -pi/4 to pi/4.
```ts
const diff = message.touch.y - paddle_y;
const scale = diff / (paddle_height * 0.5);
const rotation = (scale * math.pi / 4);
```
Then we perform the rotation on a vector pointing directly to the right, with the same length we calculated earlier. We multiply it to reverse its direction if it was originally travelling to the right.
```ts
let [new_x_velocity, new_y_velocity] = rotate(rotation, speed, 0);
new_x_velocity *= horizontal;
```
Finally, we create our velocity deltas by subtracting our old velocity from our new desired velocity.
```ts
[
velocity_message.x_delta,
velocity_message.y_delta,
] = sub(new_x_velocity, new_y_velocity, ball_velocity.x, ball_velocity.y);
```
Our final result looks like this:
```ts
import { Emits, Engine, Reads } from "encompass-ecs";
import { BoundingBoxComponent } from "game/components/bounding_box";
import { PaddleComponent } from "game/components/paddle";
import { PositionComponent } from "game/components/position";
import { VelocityComponent } from "game/components/velocity";
import { BallPaddleCollisionMessage } from "game/messages/collisions/ball_paddle";
import { UpdatePositionMessage } from "game/messages/update_position";
import { UpdateVelocityMessage } from "game/messages/update_velocity";
import { len, rotate, sub } from "lua-lib/hump/vectorlight";
@Reads(BallPaddleCollisionMessage)
@Emits(UpdatePositionMessage, UpdateVelocityMessage)
export class BallPaddleCollisionEngine extends Engine {
public update() {
for (const message of this.read_messages(BallPaddleCollisionMessage).values()) {
const ball_position = message.ball_entity.get_component(PositionComponent);
const ball_velocity = message.ball_entity.get_component(VelocityComponent);
const ball_boundaries = message.ball_entity.get_component(BoundingBoxComponent);
const paddle_height = message.paddle_entity.get_component(PaddleComponent).height;
const paddle_y = message.paddle_entity.get_component(PositionComponent).y;
const velocity_message = this.emit_component_message(UpdateVelocityMessage, ball_velocity);
// calculate new ball velocity based on paddle contact
const speed = len(ball_velocity.x, ball_velocity.y);
const horizontal = ball_velocity.x < 0 ? 1 : -1;
const diff = message.touch.y - paddle_y;
const scale = diff / (paddle_height * 0.5);
const rotation = (scale * math.pi / 4);
let [new_x_velocity, new_y_velocity] = rotate(rotation, speed, 0);
new_x_velocity *= horizontal;
[
velocity_message.x_delta,
velocity_message.y_delta,
] = sub(new_x_velocity, new_y_velocity, ball_velocity.x, ball_velocity.y);
// calculate bounce position, remembering to re-transform coordinates to origin space
const y_distance = Math.abs(message.ball_new_y - (message.touch.y + ball_boundaries.height * 0.5));
const x_distance = Math.abs(message.ball_new_x - (message.touch.x + ball_boundaries.width * 0.5));
const position_message = this.emit_component_message(UpdatePositionMessage, ball_position);
position_message.x_delta = 2 * message.normal.x * x_distance;
position_message.y_delta = 2 * message.normal.y * y_distance;
public AngledBounceMessage(Entity bouncer, Entity bounced, HitOrientation hitOrientation)
{
Bouncer = bouncer;
Bounced = bounced;
HitOrientation = hitOrientation;
}
}
}
```
We will be calculating our bounce angle based on the relative positions of the paddle and ball, so we will want a reference to both entities involved in the collision.
In **CollisionEngine.cs**:
```cs
...
CheckAngledBounce(message.EntityA, message.EntityB, message.HitOrientation);
CheckAngledBounce(message.EntityB, message.EntityA, message.HitOrientation);
...
private void CheckAngledBounce(Entity a, Entity b, HitOrientation hitOrientation)
{
if (HasComponent<CanCauseAngledBounceComponent>(a))
{
if (HasComponent<CanBeBouncedComponent>(b))
{
SendMessage(new AngledBounceMessage(a, b, hitOrientation));
}
}
}
```
One last thing. We are gonna need to know the size of our paddle and ball for our bounce calculation. We already put a scaling factor on **Texture2DComponent**. Why don't we break that out into its own component?
```cs
using Encompass;
namespace PongFE.Components
{
public struct ScaleComponent : IComponent
{
public int Width { get; }
public int Height { get; }
public ScaleComponent(int width, int height)
{
Width = width;
Height = height;
}
}
}
```
Remove Width and Height from Texture2DComponent, and add a ScaleComponent in both **PaddleSpawner** and **BallSpawner**.
Then, in **Texture2DRenderer.cs** we can do:
```cs
public override void Render(Entity entity, in Texture2DComponent textureComponent)
{
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
ref readonly var scaleComponent = ref GetComponent<ScaleComponent>(entity);
_spriteBatch.Draw(
textureComponent.Texture,
positionComponent.Position.ToXNAVector(),
null,
Color.White,
0,
Vector2.Zero,
new Vector2(scaleComponent.Width, scaleComponent.Height),
SpriteEffects.None,
0
);
}
```
Now, in **PongFE/Engines/AngledBounceEngine.cs**:
```cs
using System.Numerics;
using Encompass;
using PongFE.Components;
using PongFE.Extensions;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(
typeof(PositionComponent),
typeof(VelocityComponent),
typeof(ScaleComponent),
typeof(BounceResponseComponent)
)]
[Receives(typeof(AngledBounceMessage))]
[Sends(typeof(UpdateVelocityMessage))]
public class AngledBounceEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var message in ReadMessages<AngledBounceMessage>())
{
if (HasComponent<BounceResponseComponent>(message.Bounced))
{
ref readonly var bouncedPositionComponent = ref GetComponent<PositionComponent>(message.Bounced);
ref readonly var velocityComponent = ref GetComponent<VelocityComponent>(message.Bounced);
ref readonly var bouncedScaleComponent = ref GetComponent<ScaleComponent>(message.Bounced);
ref readonly var bouncerPositionComponent = ref GetComponent<PositionComponent>(message.Bouncer);
ref readonly var bouncerScaleComponent = ref GetComponent<ScaleComponent>(message.Bouncer);
var bouncedY = bouncedPositionComponent.Position.Y + bouncedScaleComponent.Height / 2;
var bouncerY = bouncerPositionComponent.Position.Y + bouncerScaleComponent.Height / 2;
var speed = velocityComponent.Velocity.Length();
var horizontal = velocityComponent.Velocity.X < 0 ? 1 : -1;
var diff = bouncedY - bouncerY;
var scale = (float)diff / (bouncerScaleComponent.Height / 2);
var rotation = scale * System.Math.PI / 4;
Vector2 newVelocity = new Vector2(speed, 0).Rotate((float)rotation) * new Vector2(horizontal, 1);
SendMessage(new UpdateVelocityMessage(message.Bounced, newVelocity));
}
}
}
}
}
```
Let's break down the math a bit here.
```cs
var bouncedY = bouncedPositionComponent.Position.Y + bouncedScaleComponent.Height / 2;
var bouncerY = bouncerPositionComponent.Position.Y + bouncerScaleComponent.Height / 2;
```
First, we get the center Y values by taking the Y position (which represents the top left corner) and adding half the height.
```cs
var speed = velocityComponent.Velocity.Length();
```
Remember that speed differs from velocity in that speed has no directional component. We can obtain speed from a velocity vector by taking the `Length` of the vector.
```cs
var horizontal = velocityComponent.Velocity.X < 0 ? 1 : -1;
```
If our ball is traveling to the left, we want it to bounce to the right, and vice versa.
```cs
var diff = bouncedY - bouncerY;
var scale = (float)diff / (bouncerScaleComponent.Height / 2);
var rotation = scale * System.Math.PI / 4;
```
Now we can get a new rotation. First, we figure out how far the contact was from the center of the paddle. Then we convert that to a number ranging from -1 to 1. Finally, we multiply that number by pi/4 to get a number ranging from -pi/4 to pi/4, which will be our launch angle.
```cs
Vector2 newVelocity = new Vector2(speed, 0).Rotate((float)rotation) * new Vector2(horizontal, 1);
```
First, we create a vector where the horizontal component is our speed value and the vertical component is 0. Then we rotate it by our newly created rotation angle. Finally, multiplying by [horizontal, 1] will flip the vector horizontally if *horizontal* is -1.
Make sure to add this new Engine to the WorldBuilder.
```cs
WorldBuilder.AddEngine(new AngledBounceEngine());
```
Finally, we have one last little thing to take care of - the ball moves pretty slowly right now. We also have a magic value for ball speed in the SpawnBallAfterDestroy behavior. Why not clean that up?
In **SpawnBallAfterDestroyComponent.cs**:
```cs
using Encompass;
namespace PongFE.Components
{
public struct SpawnBallAfterDestroyComponent : IComponent
{
public float Speed { get; }
public float Seconds { get; }
public SpawnBallAfterDestroyComponent(float speed, float seconds)
{
Speed = speed;
Seconds = seconds;
}
}
}
```
In **PongFEGame.cs**:
```cs
WorldBuilder.SendMessage(
new BallSpawnMessage(
new MoonTools.Structs.Position2D(PLAY_AREA_WIDTH / 2, PLAY_AREA_HEIGHT / 2),
500,
16,
16
)
);
```
And in **DestroyEngine.cs**:
```cs
SendMessage(
new BallSpawnMessage(
new MoonTools.Structs.Position2D(640, 360),
respawnComponent.Speed,
16,
16
),
respawnComponent.Seconds
);
```
Now all balls will respawn with the speed we designate in **PongFEGame.cs**.
<video width="75%" height="360" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
<source src="/images/bounce_angle.mp4" type="video/mp4">
</video>
Now we have a game on our hands!
Now this is looking more like Pong.