encompass-cs-docs/content/pong/polish/paddle_bounce.md

270 lines
8.7 KiB
Markdown

---
title: "Paddle Bounce"
date: 2019-06-08T14:46:08-07:00
weight: 10
---
One thing that isn't quite right in our game is that when the ball bounces, it reflects directly off the paddle.
If we look at the original Pong, we see that the angle of the ball is actually affected by the position where the ball hits the paddle. If the ball hits the edges of the paddle, it bounces off at a wider angle. If it hits the center of the paddle, it bounces horizontally.
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.
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.
Create a file, **PongFE/Components/CanCausedAngledBounceComponent.cs**:
```cs
using Encompass;
namespace PongFE.Components
{
public struct CanCauseAngledBounceComponent : IComponent { }
}
```
And another new file, **AngledBounceMessage.cs**:
```cs
using Encompass;
using PongFE.Enums;
namespace PongFE.Messages
{
public struct AngledBounceMessage : IMessage
{
public Entity Bounced { get; }
public Entity Bouncer { get; }
public HitOrientation HitOrientation { get; }
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 this is looking more like Pong.