rewrite decoupling and magic values sections
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
0baec62343
commit
1ca0448189
|
@ -6,7 +6,7 @@ weight: 30
|
|||
|
||||
There's more to decoupling than just objects not directly referencing each other.
|
||||
|
||||
Really, when we talk about decoupling, we are saying that we don't want the structure of the program having unnecessary direct connections.
|
||||
Really, when we talk about decoupling, we are saying that we don't want the program having unnecessary connections between structures in the code.
|
||||
|
||||
This is a pretty abstract principle, but there is a nice illustration of it in our program as it exists right now.
|
||||
|
||||
|
@ -14,7 +14,7 @@ Right now our InputEngine is sending Messages to our MotionEngine. Is that actua
|
|||
|
||||
What I am saying is... can you think of an example where something other than direct input might want to control the movement of a paddle?
|
||||
|
||||
What about AI? What about an "attract mode"?
|
||||
What about AI? What about an "attract mode", where the game plays itself?
|
||||
|
||||
There's also a lot of structure about the paddle entity being embedded into the logic of the InputEngine. That doesn't feel good to me either.
|
||||
|
||||
|
@ -22,98 +22,67 @@ Let's put something in between them.
|
|||
|
||||
We have to ask ourselves: how is it that we want the game to react when we press the buttons? We want the correct paddle to move up or down. That seems like a good abstraction for a Message.
|
||||
|
||||
Create a file: **game/engines/paddle_move_up.ts**
|
||||
Create a file: **PongFE/Messages/PaddleMoveMessage.ts**
|
||||
|
||||
```ts
|
||||
import { ComponentMessage, Message } from "encompass-ecs";
|
||||
import { PlayerComponent } from "game/components/player";
|
||||
```cs
|
||||
using Encompass;
|
||||
|
||||
export class PaddleMoveUpMessage extends Message implements ComponentMessage {
|
||||
public component: Readonly<PlayerOneComponent>;
|
||||
}
|
||||
```
|
||||
namespace PongFE.Messages
|
||||
{
|
||||
public enum PaddleMoveDirection
|
||||
{
|
||||
Up,
|
||||
Down
|
||||
}
|
||||
|
||||
and a file: **game/messages/paddle_move_down.ts**
|
||||
public struct PaddleMoveMessage : IMessage, IHasEntity
|
||||
{
|
||||
public Entity Entity { get; }
|
||||
public PaddleMoveDirection PaddleMoveDirection { get; }
|
||||
|
||||
```ts
|
||||
import { ComponentMessage, Message } from "encompass-ecs";
|
||||
import { PlayerComponent } from "game/components/player";
|
||||
|
||||
export class PaddleMoveDownMessage extends Message implements ComponentMessage {
|
||||
public component: Readonly<PlayerOneComponent>;
|
||||
}
|
||||
```
|
||||
|
||||
Uh oh, something seems weird here too. Why PlayerOneComponent? Sounds like we need some abstraction.
|
||||
|
||||
Create a file: **game/components/player.ts**
|
||||
|
||||
```ts
|
||||
import { Component } from "encompass-ecs";
|
||||
|
||||
export abstract class PlayerComponent extends Component {}
|
||||
```
|
||||
|
||||
*abstract* means that the class cannot be used directly, but must be inherited.
|
||||
|
||||
Change **games/component/player_one.ts**:
|
||||
|
||||
```ts
|
||||
import { PlayerComponent } from "./player";
|
||||
|
||||
export class PlayerOneComponent extends PlayerComponent {}
|
||||
```
|
||||
|
||||
And create **games/component/player_two.ts**:
|
||||
|
||||
```ts
|
||||
import { PlayerComponent } from "./player";
|
||||
|
||||
export class PlayerTwoComponent extends PlayerComponent {}
|
||||
```
|
||||
|
||||
One more thing: Why do we have separate MoveUp and MoveDown messages? The only distinction is the direction, which can easily be represented by a number. Let's consolidate everything.
|
||||
|
||||
Delete our PaddleMoveUpMessage and PaddleMoveDownMessage.
|
||||
|
||||
Create a file: **game/messages/paddle_move.ts**
|
||||
|
||||
```ts
|
||||
import { ComponentMessage, Message } from "encompass-ecs";
|
||||
import { PlayerComponent } from "game/components/player";
|
||||
|
||||
export class PaddleMoveMessage extends Message implements ComponentMessage {
|
||||
public component: Readonly<PlayerComponent>;
|
||||
public direction: number;
|
||||
public PaddleMoveMessage(Entity entity, PaddleMoveDirection paddleMoveDirection)
|
||||
{
|
||||
Entity = entity;
|
||||
PaddleMoveDirection = paddleMoveDirection;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now we're ready to rumble!!
|
||||
|
||||
Create a file: **game/engines/paddle_movement.ts**
|
||||
Create a file: **PongFE/Engines/PaddleMovementEngine.cs**
|
||||
|
||||
```ts
|
||||
import { Emits, Engine, Reads } from "encompass-ecs";
|
||||
import { PaddleMoveSpeedComponent } from "game/components/paddle_move_speed";
|
||||
import { PositionComponent } from "game/components/position";
|
||||
import { MotionMessage } from "game/messages/component/motion";
|
||||
import { PaddleMoveMessage } from "game/messages/component/paddle_move";
|
||||
```cs
|
||||
using Encompass;
|
||||
using PongFE.Components;
|
||||
using PongFE.Messages;
|
||||
|
||||
@Reads(PaddleMoveMessage)
|
||||
@Emits(MotionMessage)
|
||||
export class PaddleMovementEngine extends Engine {
|
||||
public update() {
|
||||
for (const message of this.read_messages(PaddleMoveMessage).values()) {
|
||||
const player_component = message.component;
|
||||
const player_entity = this.get_entity(player_component.entity_id);
|
||||
namespace PongFE.Engines
|
||||
{
|
||||
[Reads(typeof(PaddleMoveSpeedComponent))]
|
||||
[Receives(typeof(PaddleMoveMessage))]
|
||||
[Sends(typeof(MotionMessage))]
|
||||
public class PaddleMovementEngine : Engine
|
||||
{
|
||||
public override void Update(double dt)
|
||||
{
|
||||
foreach (ref readonly var message in ReadMessages<PaddleMoveMessage>())
|
||||
{
|
||||
if (HasComponent<PaddleMoveSpeedComponent>(message.Entity))
|
||||
{
|
||||
var directionMultiplier = message.PaddleMoveDirection == PaddleMoveDirection.Down ? 1 : -1;
|
||||
|
||||
if (player_entity) {
|
||||
const position_component = player_entity.get_component(PositionComponent);
|
||||
const move_speed_component = player_entity.get_component(PaddleMoveSpeedComponent);
|
||||
ref readonly var paddleMoveSpeedComponent =
|
||||
ref GetComponent<PaddleMoveSpeedComponent>(message.Entity);
|
||||
|
||||
const motion_message = this.emit_component_message(MotionMessage, position_component);
|
||||
motion_message.x = 0;
|
||||
motion_message.y = message.direction * move_speed_component.y;
|
||||
SendMessage(
|
||||
new MotionMessage(
|
||||
message.Entity,
|
||||
new System.Numerics.Vector2(0, paddleMoveSpeedComponent.Speed * directionMultiplier * (float)dt)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,49 +91,76 @@ export class PaddleMovementEngine extends Engine {
|
|||
|
||||
Finally, we revise our InputEngine to send PaddleMoveMessages.
|
||||
|
||||
```ts
|
||||
import { Emits, Engine } from "encompass-ecs";
|
||||
import { PlayerOneComponent } from "game/components/player_one";
|
||||
import { PaddleMoveMessage } from "game/messages/component/paddle_move";
|
||||
```cs
|
||||
using Encompass;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using PongFE.Components;
|
||||
using PongFE.Messages;
|
||||
|
||||
@Emits(PaddleMoveMessage)
|
||||
export class InputEngine extends Engine {
|
||||
public update() {
|
||||
const player_one_component = this.read_component(PlayerOneComponent);
|
||||
namespace PongFE.Engines
|
||||
{
|
||||
[Reads(typeof(PlayerInputComponent))]
|
||||
[Sends(typeof(PaddleMoveMessage))]
|
||||
public class InputEngine : Engine
|
||||
{
|
||||
public override void Update(double dt)
|
||||
{
|
||||
var keyboardState = Keyboard.GetState();
|
||||
|
||||
if (player_one_component) {
|
||||
const player_one_entity = this.get_entity(player_one_component.entity_id);
|
||||
foreach (ref readonly var playerInputEntity in ReadEntities<PlayerInputComponent>())
|
||||
{
|
||||
ref readonly var playerInputComponent = ref GetComponent<PlayerInputComponent>(playerInputEntity);
|
||||
|
||||
if (player_one_entity) {
|
||||
if (love.keyboard.isDown("up")) {
|
||||
const message = this.emit_component_message(PaddleMoveMessage, player_one_component);
|
||||
message.direction = -1;
|
||||
} else if (love.keyboard.isDown("down")) {
|
||||
const message = this.emit_component_message(PaddleMoveMessage, player_one_component);
|
||||
message.direction = 1;
|
||||
if (playerInputComponent.PlayerIndex == PlayerIndex.One)
|
||||
{
|
||||
if (keyboardState.IsKeyDown(Keys.Down))
|
||||
{
|
||||
SendMessage(
|
||||
new PaddleMoveMessage(
|
||||
playerInputEntity,
|
||||
PaddleMoveDirection.Down
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (keyboardState.IsKeyDown(Keys.Up))
|
||||
{
|
||||
SendMessage(
|
||||
new PaddleMoveMessage(
|
||||
playerInputEntity,
|
||||
PaddleMoveDirection.Up
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
And don't forgot to add our new Engine to the WorldBuilder.
|
||||
|
||||
```ts
|
||||
world_builder.add_engine(PaddleMovementEngine);
|
||||
```cs
|
||||
...
|
||||
|
||||
WorldBuilder.AddEngine(new InputEngine());
|
||||
WorldBuilder.AddEngine(new PaddleMovementEngine());
|
||||
WorldBuilder.AddEngine(new MotionEngine());
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Look at how concise and easy to understand everything is now! And when we get around to adding Player 2 we'll barely have to do any work at all, because everything that drives Player 1 can be used to drive Player 2.
|
||||
Look at how concise and easy to understand everything is now! If each of your engines has a very precise and simple to explain job, you are in a good place. InputEngine's job is to detect input and send messages in response to the input. PaddleMovementEngine's job is to check for paddle movement messages and move the appropriate paddle. And MotionEngine's job is to move entities. And when we get around to adding Player 2 we'll barely have to do any work at all, because everything that drives Player 1 can be used to drive Player 2.
|
||||
|
||||
Decoupling isn't just a law we follow just because someone told us it was a good idea or whatever. It exists as a principle to remind us to try to express our ideas at a higher level, so we can write more powerful and concise code and create less room for error.
|
||||
Decoupling isn't just a law we follow just because someone told us it was a good idea or whatever. It exists as a principle to remind us to try to express our ideas at a higher level, so we can write more powerful, flexible, and concise code and create less room for error.
|
||||
|
||||
When you get more experienced you'll be able to sniff out tightly coupled code very easily. Don't stress about it too much, but keep the idea in the back of your mind always.
|
||||
When you get more experienced you'll be able to sniff out tightly coupled code very easily. Don't stress about it too much, but always be thinking about how you can make your code structure more expressive and easy to understand and reuse.
|
||||
|
||||
Before we move on, let's make the paddle a little zippier.
|
||||
|
||||
```ts
|
||||
move_speed_component.y = 400;
|
||||
```cs
|
||||
WorldBuilder.SetComponent(paddle, new PaddleMoveSpeedComponent(400));
|
||||
```
|
||||
|
||||
<video width="75%" autoplay="autoplay" muted="muted" loop="loop" style="display: block; margin: 0 auto;">
|
||||
|
|
|
@ -6,64 +6,102 @@ weight: 25
|
|||
|
||||
Our code right now is violating one more good architecture principle.
|
||||
|
||||
```ts
|
||||
if (love.keyboard.isDown("up")) {
|
||||
const message = this.emit_component_message(MotionMessage, player_one_position_component);
|
||||
message.x = 0;
|
||||
message.y = -10;
|
||||
}
|
||||
```cs
|
||||
...
|
||||
|
||||
if (keyboardState.IsKeyDown(Keys.Down))
|
||||
{
|
||||
SendMessage(
|
||||
new MotionMessage(playerInputEntity,
|
||||
new System.Numerics.Vector2(0, 10 * (float)dt))
|
||||
);
|
||||
}
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
*Magic values* refer to numbers that have been placed directly in the code. The values being set to *message.x* and *message.y* above are magic values. Why are magic values bad?
|
||||
*Magic values* refer to numbers that have been placed directly in the code. That 10 above is a magic value. Why are magic values bad?
|
||||
|
||||
Magic values introduce the possibility of duplication. Let's say I start adding my code to be able to move paddles down. Now I have to change the numbers in two different places. If I ever change one without changing the other, I have introduced a bug.
|
||||
|
||||
There's another reason to avoid magic values too. Suppose I haven't looked at the InputEngine for a while, but I suddenly decide that the paddles are moving too slow. Intuitively, I would want to look for a Component that contains those values, but instead, they would be hidden in the InputEngine. This isn't what you would really expect - why would Input have anything to do with the speed of the paddles?
|
||||
There's another reason to avoid magic values too. Suppose I haven't looked at the InputEngine for a while, but I suddenly decide that the paddles are moving too slow. Intuitively, I would want to look for a Component that contains those values, but instead, they would be hidden in the InputEngine. This isn't what you would really expect - why should Input have anything to do with the speed of the paddles?
|
||||
|
||||
Organizing your information consistently is crucial to being able to easily find things that you need to change. You'll thank yourself later.
|
||||
|
||||
Let's make a new Component.
|
||||
|
||||
Create a file: **game/components/paddle_move_speed.ts**
|
||||
Create a file: **PongFE/Components/PaddleMoveSpeedComponent.cs**
|
||||
|
||||
```ts
|
||||
import { Component } from "encompass-ecs";
|
||||
```cs
|
||||
using Encompass;
|
||||
|
||||
export class PaddleMoveSpeedComponent extends Component {
|
||||
public y: number;
|
||||
namespace PongFE.Components
|
||||
{
|
||||
public struct PaddleMoveSpeedComponent : IComponent
|
||||
{
|
||||
public float Speed { get; }
|
||||
|
||||
public PaddleMoveSpeedComponent(float speed)
|
||||
{
|
||||
Speed = speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And let's add it to our paddle Entity.
|
||||
|
||||
In **game/game.ts**
|
||||
In **PongFEGame.ts**
|
||||
|
||||
```ts
|
||||
const move_speed_component = paddle_entity.add_component(PaddleMoveSpeedComponent);
|
||||
move_speed_component.y = 10;
|
||||
```cs
|
||||
...
|
||||
|
||||
WorldBuilder.SetComponent(paddle, new PlayerInputComponent(PongFE.Components.PlayerIndex.One));
|
||||
WorldBuilder.SetComponent(paddle, new PaddleMoveSpeedComponent(10));
|
||||
WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
Now let's tell our InputEngine to use it, and why don't we go ahead and make the "down" key move the paddle downward too.
|
||||
Now let's tell our InputEngine to use it, and why don't we go ahead and make the Up key move the paddle upward too.
|
||||
|
||||
```ts
|
||||
...
|
||||
```cs
|
||||
...
|
||||
|
||||
if (player_one_entity) {
|
||||
const player_one_position_component = player_one_entity.get_component(PositionComponent);
|
||||
const player_one_move_speed_component = player_one_entity.get_component(PaddleMoveSpeedComponent);
|
||||
foreach (ref readonly var playerInputEntity in ReadEntities<PlayerInputComponent>())
|
||||
{
|
||||
ref readonly var playerInputComponent = ref GetComponent<PlayerInputComponent>(playerInputEntity);
|
||||
|
||||
if (love.keyboard.isDown("up")) {
|
||||
const message = this.emit_component_message(MotionMessage, player_one_position_component);
|
||||
message.x = 0;
|
||||
message.y = -player_one_move_speed_component.y;
|
||||
} else if (love.keyboard.isDown("down")) {
|
||||
const message = this.emit_component_message(MotionMessage, player_one_position_component);
|
||||
message.x = 0;
|
||||
message.y = player_one_move_speed_component.y;
|
||||
if (HasComponent<PaddleMoveSpeedComponent>(playerInputEntity))
|
||||
{
|
||||
ref readonly var paddleMoveSpeedComponent = ref GetComponent<PaddleMoveSpeedComponent>(playerInputEntity);
|
||||
var paddleSpeed = paddleMoveSpeedComponent.Speed;
|
||||
|
||||
if (playerInputComponent.PlayerIndex == PlayerIndex.One)
|
||||
{
|
||||
if (keyboardState.IsKeyDown(Keys.Down))
|
||||
{
|
||||
SendMessage(
|
||||
new MotionMessage(
|
||||
playerInputEntity,
|
||||
new System.Numerics.Vector2(0, paddleSpeed * (float)dt)
|
||||
)
|
||||
);
|
||||
}
|
||||
else if (keyboardState.IsKeyDown(Keys.Up))
|
||||
{
|
||||
SendMessage(
|
||||
new MotionMessage(
|
||||
playerInputEntity,
|
||||
new System.Numerics.Vector2(0, paddleSpeed * (float)dt)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
...
|
||||
```
|
||||
|
||||
I'm starting to get get a bad feeling about this. Are you? Let me explain.
|
||||
I'm starting to get get a bad feeling about this. Let me explain.
|
||||
|
|
Loading…
Reference in New Issue