some paddle movement stuff
continuous-integration/drone/push Build is passing Details

main
Evan Hemsley 2020-07-12 13:40:19 -07:00
parent 84ecc93076
commit 0baec62343
5 changed files with 244 additions and 155 deletions

View File

@ -4,7 +4,7 @@ date: 2019-05-22T10:38:01-07:00
weight: 12
---
FNA is a game development framework that uses C# as its scripting language.
FNA is a game development framework that uses C# as its scripting language.
It is an accurate re-implementation of the XNA 4.0 API, and has been used to ship several dozen successful games.
I like using FNA because it is highly stable, time-tested, low level enough to be flexible and high-level enough to be easy to use. It's really great!
@ -13,13 +13,15 @@ If you are new to C# scripting, Microsoft has a decent high-level intro here: ht
I recommend giving the FNA wiki a quick read as an intro: https://github.com/FNA-XNA/FNA/wiki
I have created a template for getting started with an empty FNA project: https://gitea.moonside.games/MoonsideGames/FNA-VSCode-Template
Download it and follow the install instructions.
I have created a template for getting started with an empty Encompass/FNA project: https://gitea.moonside.games/MoonsideGames/Encompass-FNA-Template
If you are using VSCode, you can hit Ctrl-Shift-B to bring up the build menu.
Download it and follow the install instructions.
If you are using VSCode, you can hit Ctrl-Shift-B to bring up the build menu.
`Framework` tasks will execute using .NET Framework or Mono, and `Core` tasks will execute using .NET Core runtime.
If you are not using VSCode, you can execute build and execute tasks from the command line.
I assume if you are savvy enough to not be using some kind of IDE you probably don't need handholding here, but let me know if I'm wrong!
If you are not using VSCode, you can build and execute from the command line.
I assume if you are savvy enough to not be using some kind of IDE you probably don't need handholding here.
Just run msbuild or dotnet build from the command line. But let me know if I'm wrong!
Once you can Build and Run and the cornflower blue screen of life pops up, we are ready to go!

View File

@ -4,8 +4,6 @@ date: 2019-05-23T12:59:29-07:00
weight: 100
---
Now we want to drive the simulation: we want values to change over time.
So we are gonna need some Engines.
Now we want to drive the simulation. Games are only interesting because things change over time. So we are gonna need some Engines.
Let's write our first Engine.

View File

@ -4,6 +4,22 @@ date: 2019-05-23T14:12:20-07:00
weight: 20
---
Let's do a little experiment. In the PongFEGame constructor, add the following line:
```cs
...
Window.AllowUserResizing = true;
IsMouseVisible = true;
IsFixedTimeStep = false;
...
```
This means that the game will now run with an *unlocked timestep*. In other words, the game will update as fast as the computer allows us to. By default, FNA tries to make the game update at a rate of 60 times per second.
Now what happens if we run the game?
![oh dear](/images/oh_dear.gif)
Oh dear. That doesn't seem right at all.
@ -12,7 +28,9 @@ The paddle is moving way too fast. But our speed value is only 10. What's going
Remember when I mentioned *frame-dependence* and *delta-time* earlier? This is a classic example of frame-dependence. Notice that the FPS, or frames-per-second, of the game is around 500 in the above recording. Our motion message when we press the "up" key on the keyboard tells the paddle to move 10 units.
That means every frame we have the "up" key held down, the paddle is moving 10 units. Which means, as things stand right now, the paddle is moving about 5000 units per second. And if the framerate changes for some reason, the paddle will move slower or quicker. This means the actual progress of the simulation will be completely different on slower or faster computers.
That means every frame we have the Down key held down, the paddle is moving 10 units. Which means, as things stand right now, the paddle is moving about 5000 units per second. And if the framerate changes for some reason, the paddle will move slower or quicker. This means the actual progress of the simulation will be completely different on slower or faster computers.
Even if you use fixed timestep, what if you want to lock the timestep to 30 later? Or 120? The entire behavior of your game will change.
This is where *delta-time* comes in to save the day.
@ -20,11 +38,13 @@ This is where *delta-time* comes in to save the day.
If we multiply the rate of change of the position by delta-time, then the paddle will move at the same speed no matter whether the framerate changes.
Let's go back to the lines of the MotionEngine where we update the position.
Let's go back to the line of the InputEngine where we send the MotionMessage.
```ts
position_component.x += message.x * dt;
position_component.y += message.y * dt;
```cs
SendMessage(new MotionMessage(
playerInputEntity,
new System.Numerics.Vector2(0, 10 * (float)dt))
);
```
![better](/images/better.gif)

View File

@ -8,18 +8,24 @@ In Pong, the paddles move when the player moves the joystick on their controller
We currently have a MotionEngine that reads MotionMessages and moves the PositionComponents they reference.
So... it makes sense that we would have an InputEngine that sends MotionMessages, yeah?
So... it makes sense that we could have an InputEngine that sends MotionMessages, yeah?
Create a file: **game/engines/input.ts**
Create a file: **PongFE/Engines/InputEngine.cs**
```ts
import { Engine } from "encompass-ecs";
import { MotionMessage } from "game/messages/component/motion";
```cs
using Encompass;
using Microsoft.Xna.Framework.Input;
using PongFE.Messages;
export class InputEngine extends Engine {
public update() {
if (love.keyboard.isDown("up")) {
this.emit_component_message(MotionMessage,
public class InputEngine : Engine
{
public override void Update(double dt)
{
var keyboardState = Keyboard.GetState();
if (keyboardState.IsKeyDown(Keys.Down))
{
SendMessage(new MotionMessage(
}
}
}
@ -27,57 +33,83 @@ export class InputEngine extends Engine {
*record scratch*
Uh oh. *Engine.emit_component_message* emits a Component Message, as the name suggests. But it needs an actual component to attach to the message. How do we give the message a reference to our paddle entity's position?
Uh oh. **SendMessage** emits a message, as the name suggests, and we can use it to send out a MotionMessage. But it needs a reference to our paddle entity.
Sounds like we need another Component.
At this point, you might be tempted to attach our paddle entity as a property on InputEngine. This would be a *terrible mistake*. We *absolutely never* want to have our game state directly attached to the state of an Engine.
One thing we can use Components for is a concept I call *marking* or *tagging*. Essentially, we use a Component to designate that an Entity is a certain kind of object in the game.
What we want instead is to have a component that the InputEngine can use to look up the appropriate entity. This is a concept I refer to as *tagging*. Essentially, we use a Component to designate that an Entity is a certain kind of object that we want to be able to reference.
Create a file: **component/player_one.ts**
Create a file: **PongFE/Components/PlayerInputComponent.cs**
```ts
import { Component } from "encompass-ecs";
```cs
using Encompass;
namespace PongFE.Components
{
public enum PlayerIndex
{
One,
Two
}
public struct PlayerInputComponent : IComponent
{
public PlayerIndex PlayerIndex { get; }
public PlayerInputComponent(PlayerIndex playerIndex)
{
PlayerIndex = playerIndex;
}
}
}
export class PlayerOneComponent extends Component {}
```
That's it! The component itself doesn't need any information on it. Its mere existence on the Entity will tell us that this Entity represents Player 1.
Why an *enum* instead of just an integer or something? When we write programs it is very easy to shoot ourselves in the foot. What if someone accidentally typed -1 in as a value or something? Enums structure our data to make it harder for us to make silly mistakes like this.
Let's add it to our paddle Entity.
Let's add this component to our paddle entity.
In **game/game.ts**:
In **PongFEGame.cs**:
```ts
...
```cs
...
paddle_entity.add_component(PlayerOneComponent);
var paddle = WorldBuilder.CreateEntity();
WorldBuilder.SetComponent(paddle, new PlayerInputComponent(PongFE.Components.PlayerIndex.One));
WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
WorldBuilder.SetComponent(paddle, new Texture2DComponent(PaddleTexture, 0));
...
...
```
Now we can go back to our InputEngine.
```ts
import { Emits, Engine } from "encompass-ecs";
import { PlayerOneComponent } from "game/components/player_one";
import { PositionComponent } from "game/components/position";
import { MotionMessage } from "game/messages/component/motion";
```cs
using Encompass;
using Microsoft.Xna.Framework.Input;
using PongFE.Components;
using PongFE.Messages;
@Emits(MotionMessage)
export class InputEngine extends Engine {
public update() {
const player_one_component = this.read_component(PlayerOneComponent);
namespace PongFE.Engines
{
[Reads(typeof(PlayerInputComponent))]
[Sends(typeof(MotionMessage))]
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) {
const player_one_position_component = player_one_entity.get_component(PositionComponent);
if (love.keyboard.isDown("up")) {
const message = this.emit_component_message(MotionMessage, player_one_position_component);
message.x = 0;
message.y = -10;
if (playerInputComponent.PlayerIndex == PlayerIndex.One)
{
if (keyboardState.IsKeyDown(Keys.Down))
{
SendMessage(new MotionMessage(playerInputEntity, new System.Numerics.Vector2(0, 10)));
}
}
}
}
@ -85,28 +117,22 @@ export class InputEngine extends Engine {
}
```
Ok... what the heck is *this.read_component*?
Engines have total freedom to read anything in the game state that they desire. This gives Engines a lot of flexibility to do what they need to do. In this case, **ReadEntities<PlayerInputComponent>** lets us get a reference to each entity that has a PlayerInputComponent attached to it. From there, we can get the specific PlayerInputComponent for that Entity, check which PlayerIndex it contains, and then send a message to its entity if the Down key is pressed.
Engines have total freedom to read anything in the game state that they desire. This gives Engines a tremendous amount of flexibility to do what they need to do.
Also, remember when we had to declare **Reads** and **Receives** and **Writes** on our MotionEngine? Well, similarly, we have to declare **Sends** when our engine emits a certain kind of Message. Otherwise Encompass will get mad at us and crash the game for our own safety.
In this case, we are reading the game state to find our PlayerOneComponent. From there, we can get the Entity to which the PlayerOneComponent belongs. Then we can get the PositionComponent of that Entity, and send a message about it when the "up" key is pressed down.
Let's add our PlayerInputEngine to the WorldBuilder.
{{% notice warning %}}
Similar to *Entity.get_component* and *Entity.get_components*, Engines have *Engine.read_component* and *Engine.read_components*. If you try to do the singular *read_component* on a game state that has multiple components of that type, an error will be thrown. So be careful that your singleton components are actually singletons!
{{% /notice %}}
In **PongFEGame.cs**:
Also, remember when we had to declare **@Reads** on our MotionEngine? Well, similarly, we have to declare **@Emits** when our Engine emits a certain kind of Message. Otherwise Encompass will get mad at us and crash the game for our own safety.
```cs
...
Let's add our InputEngine to the WorldBuilder.
WorldBuilder.AddEngine(new InputEngine());
WorldBuilder.AddEngine(new MotionEngine());
WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));
In **game/game.ts**:
```ts
// ADD YOUR ENGINES HERE...
world_builder.add_engine(InputEngine);
world_builder.add_engine(MotionEngine);
...
```
It doesn't matter which order they go in, because remember, Encompass figures it out automatically. I just prefer this order for some reason. Once we have a lot of Engines it stops mattering pretty quickly anyway.
Let's run the game again!!

View File

@ -6,129 +6,172 @@ weight: 5
To create an Engine, we extend the Engine class.
Create a file: **game/engines/motion.ts**
Create a file: **PongFE/Engines/MotionEngine.cs**
```ts
import { Engine } from "encompass-ecs";
```cs
using Encompass;
export class MotionEngine extends Engine {
public update(dt: number) {}
public class MotionEngine : Engine
{
public override void Update(double dt)
{
}
}
```
Every Engine needs an *update* method, which optionally takes a *delta-time* value as a parameter.
Every Engine must implement an *Update* method, which takes a *delta-time* value as a parameter.
*delta-time* is simply the time that has elapsed between the last frame and the current one in seconds. We'll talk more about why this is important in a minute.
Let's think for a minute about what we want this Engine to actually *do*. Motion is just the change of position over time, right? So our MotionEngine is going to modify PositionComponents based on some amount of movement.
Let's think for a minute about what we want this Engine to actually do. Motion is just the change of position over time, right? So our MotionEngine is going to modify PositionComponents based on some amount of movement.
We're gonna need a Message. More specifically, a ComponentMessage.
We're gonna need a Message.
Create a file: **game/messages/component/motion.ts**
Create a file: **PongFE/Messages/MotionMessage.cs**
```ts
import { ComponentMessage, Message } from "encompass-ecs";
import { PositionComponent } from "game/components/position";
```cs
using System.Numerics;
using Encompass;
export class MotionMessage extends Message implements ComponentMessage {
public component: Readonly<PositionComponent>;
public x: number;
public y: number;
namespace PongFE.Messages
{
public struct MotionMessage : IMessage, IHasEntity
{
public Entity Entity { get; }
public Vector2 Movement { get; }
public MotionMessage(Entity entity, Vector2 movement)
{
Entity = entity;
Movement = movement;
}
}
}
```
*implements* means that the class defines certain required properties or methods. If you don't understand it right now, don't worry, just know that in this case, a Message that *implements* ComponentMessage needs to have a *component* property. In our case, a MotionMessage wants to refer to some specific PositionComponent that needs to be updated.
Similar to a component, a message is a struct which implements the *IMessage* interface. Also, motion is something that refers to a specific object, right? So we want our message to have a reference to an entity. We can declare the *IHasEntity* interface on our message, which allows Encompass to perform certain lookup optimizations. We'll talk about that in a second.
{{% notice warning %}}
Why is the component type wrapped in *Readonly*? You can actually get away with not doing this, but it means you can accidentally get around some of the safety features of Encompass that prevent race conditions. So make sure you do this when defining a ComponentMessage.
{{% /notice %}}
{{% notice tip %}}
Remember before when I said that it is a big no-no to have Components reference each other? Well, it's perfectly fine to have Messages refer to a Component, or even multiple Components.
**Don't** ever have a Message that refers to another Message though. That is very bad.
Don't **ever** have a Message that refers to another Message. That is very bad.
{{% /notice %}}
Now, how is our MotionEngine going to interact with MotionMessages? It's going to Read them.
```ts
import { Engine, Reads } from "encompass-ecs";
import { MotionMessage } from "game/messages/component/motion";
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
@Reads(MotionMessage)
export class MotionEngine extends Engine {
public update(dt: number) {
const motion_messages = this.read_messages(MotionMessage);
}
}
```
namespace PongFE.Engines
{
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
What happens if we don't declare **@Reads** but still call *read_messages*? Encompass will yell at us when the game runs, because then it can't guarantee that this Engine runs after Engines which *emit* MotionMessages, which is no good. We'll talk about Emitting messages soon.
Now we have a reference to all MotionMessages that were emitted this frame. Let's use them to update PositionComponents.
```ts
import { Engine, Reads } from "encompass-ecs";
import { MotionMessage } from "game/messages/component/motion";
@Reads(MotionMessage)
export class MotionEngine extends Engine {
public update(dt: number) {
const motion_messages = this.read_messages(MotionMessage);
for (const message of motion_messages.values()) {
const position_component = message.component;
position_component.x += message.x;
position_component.y += message.y;
}
}
}
}
```
Uh oh. The compiler is yelling at us. "Cannot assign to 'x' because it is a read-only property." We're going to need to make the component Mutable.
If we run the game right now, Encompass will yell at us. Why? Because it can't guarantee that this Engine runs after Engines which *send* MotionMessages, which is no good. So we need to declare what is called a *class attribute* on the Engine. We'll talk about sending messages soon. But for now, we need to let Encompass know that this Engine will be receiving MotionMessages with a *Receives* attribute.
Mutable is a scary word, but it really just means "can have its properties changed." We *really* don't want two different Engines to be able to change the same Component type, because then we can't be certain about what the final result of the changes will be, and that is an opportunity for horrible nasty bugs to lurk in our game.
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
So if we're going to be changing PositionComponents, the Engine needs to declare that it Mutates them, and then make the Component mutable.
namespace PongFE.Engines
{
[Receives(typeof(MotionMessage))]
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
```ts
import { Engine, Mutates, Reads } from "encompass-ecs";
import { PositionComponent } from "game/components/position";
import { MotionMessage } from "game/messages/component/motion";
@Reads(MotionMessage)
@Mutates(PositionComponent)
export class MotionEngine extends Engine {
public update(dt: number) {
const motion_messages = this.read_messages(MotionMessage);
for (const message of motion_messages.values()) {
const position_component = this.make_mutable(message.component);
position_component.x += message.x;
position_component.y += message.y;
}
}
}
}
```
Now the compiler is content, and so are we.
Now the program will run without error. Let's use the MotionMessages to update PositionComponents.
Let's add this Engine to our WorldBuilder before we forget.
In **game/game.ts**
```ts
...
public load() {
this.canvas = love.graphics.newCanvas();
const world_builder = new WorldBuilder();
// ADD YOUR ENGINES HERE...
world_builder.add_engine(MotionEngine);
...
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Receives(typeof(MotionMessage))]
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
if (HasComponent<PositionComponent>(motionMessage.Entity))
{
ref readonly var positionComponent = ref GetComponent<PositionComponent>(motionMessage.Entity);
var newPosition = positionComponent.Position + motionMessage.Movement;
SetComponent(motionMessage.Entity, new PositionComponent(newPosition));
}
}
}
}
}
```
We have a couple of new methods showing up here. The first is **HasComponent**. This method simply returns true if the given Entity has a component of the given type, and false otherwise. It's usually a good idea to call this method before using **GetComponent** unless we are in a case where we already know for sure that the Entity will have that component type.
Next we have **SetComponent**. Remember that we can't just change component values wherever we want, because **GetComponent** returns readonly references. This is for our own good! Requiring explicit component updates prevents you from shooting yourself in the foot by changing values in unexpected places. Every component update is a place for horrible nasty bugs to lurk in our game, so we want to take care and be smart about when we do it.
Each Entity can only have a single component of each type. So calling SetComponent will overwrite the component, or just attach the component if one of that type didn't exist on the entity beforehand.
Finally, if we're going to be reading and changing PositionComponents, the Engine needs to declare that it Reads and Writes them.
```cs
using Encompass;
using PongFE.Components;
using PongFE.Messages;
namespace PongFE.Engines
{
[Reads(typeof(PositionComponent))]
[Receives(typeof(MotionMessage))]
[Writes(typeof(PositionComponent))]
public class MotionEngine : Engine
{
public override void Update(double dt)
{
foreach (ref readonly var motionMessage in ReadMessages<MotionMessage>())
{
if (HasComponent<PositionComponent>(motionMessage.Entity))
{
ref readonly var positionComponent = ref GetComponent<PositionComponent>(motionMessage.Entity);
var newPosition = positionComponent.Position + motionMessage.Movement;
SetComponent(motionMessage.Entity, new PositionComponent(newPosition));
}
}
}
}
}
```
Before we move on any farther, let's make sure to add this Engine to our WorldBuilder.
```cs
...
WorldBuilder.AddEngine(new MotionEngine());
WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));
...
```
Of course, if we run the game now, nothing will happen, because nothing is actually sending out MotionMessages. Let's make that happen.