diff --git a/content/pong/draw_paddle/_index.md b/content/pong/draw_paddle/_index.md index 428de9b..5245d91 100644 --- a/content/pong/draw_paddle/_index.md +++ b/content/pong/draw_paddle/_index.md @@ -1,7 +1,7 @@ --- -title: "Drawing A Paddle" +title: "Drawing a Paddle" date: 2019-05-23T11:02:45-07:00 -weight: 0 +weight: 10 --- It's nice to see something on screen right away when we start making a game, so let's make that happen. diff --git a/content/pong/draw_paddle/canvas_component.md b/content/pong/draw_paddle/canvas_component.md index 2abb173..a5b4bc6 100644 --- a/content/pong/draw_paddle/canvas_component.md +++ b/content/pong/draw_paddle/canvas_component.md @@ -6,7 +6,7 @@ weight: 5 LOVE provides a neat little drawing feature called Canvases. You can tell LOVE to draw to a Canvas instead of the screen, and then save the Canvas so you don't have to repeat lots of draw procedures. It's very nifty. -Let's set up a CanvasComponent. +Let's set up a CanvasComponent. To create a new Component type, we extend the Component class. Create a file: **game/components/canvas.ts** diff --git a/content/pong/introduction.md b/content/pong/introduction.md index b7859dd..c2f3878 100644 --- a/content/pong/introduction.md +++ b/content/pong/introduction.md @@ -8,6 +8,6 @@ Everyone has played, or at least heard of, Pong. Right? Right... ![pong](/images/pong.png) -Pong was one of the first video games ever created and as such, it is extremely simple, and I think it's a good choice to try re-implementing it in Encompass as an example. +Pong was one of the first video games ever created and as such, it is extremely simple. We're introducing a lot of new concepts with Encompass and the Hyper ECS architecture, so I think it's a good choice to try re-implementing the very simple Pong in Encompass as an example. We'll be developing this with the Encompass/LOVE starter pack. Go ahead and [set that up](/getting_started/case_study_love/) if you haven't already so you can follow along. And please do follow along - you can do it! diff --git a/content/pong/move_paddle/_index.md b/content/pong/move_paddle/_index.md new file mode 100644 index 0000000..8804aff --- /dev/null +++ b/content/pong/move_paddle/_index.md @@ -0,0 +1,11 @@ +--- +title: "Moving the Paddle" +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. + +Let's write our first Engine. diff --git a/content/pong/move_paddle/frame_dependence.md b/content/pong/move_paddle/frame_dependence.md new file mode 100644 index 0000000..ba47cc1 --- /dev/null +++ b/content/pong/move_paddle/frame_dependence.md @@ -0,0 +1,34 @@ +--- +title: "Frame-Dependence" +date: 2019-05-23T14:12:20-07:00 +weight: 20 +--- + +![oh dear](/images/oh_dear.gif) + +Oh dear. That doesn't seem right at all. + +The paddle is moving way too fast. But our speed value is only 10. What's going on? + +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. + +This is where *delta-time* comes in to save the day. + +*delta-time*, as I mentioned before, is the amount of time that has passed between the previous frame and the current frame. + +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. + +```ts +position_component.x += message.x * dt; +position_component.y += message.y * dt; +``` + +![better](/images/better.gif) + +Our simulation is now *frame-independent*, which is what we desire. The paddle is definitely moving too slowly now, but that's something we can fix with a bit of value tweaking. + +It is very important that you take care to multiply things that change over time by the delta-time value, or you risk elements of your game becoming frame-dependent. diff --git a/content/pong/move_paddle/input_handling.md b/content/pong/move_paddle/input_handling.md new file mode 100644 index 0000000..e1572f6 --- /dev/null +++ b/content/pong/move_paddle/input_handling.md @@ -0,0 +1,112 @@ +--- +title: "Input Handling" +date: 2019-05-23T13:38:42-07:00 +weight: 10 +--- + +In Pong, the paddles move when the player moves the joystick on their controller up or down. + +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? + +Create a file: **game/engines/input.ts** + +```ts +import { Engine } from "encompass-ecs"; +import { MotionMessage } from "game/messages/component/motion"; + +export class InputEngine extends Engine { + public update() { + if (love.keyboard.isDown("up")) { + this.emit_component_message(MotionMessage, + } + } +} +``` + +*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? + +Sounds like we need another Component. + +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. + +Create a file: **component/player_one.ts** + +```ts +import { Component } from "encompass-ecs"; + +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. + +Let's add it to our paddle Entity. + +In **game/game.ts**: + +```ts +... + +paddle_entity.add_component(PlayerOneComponent); + +... +``` + +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"; + +@Emits(MotionMessage) +export class InputEngine extends Engine { + public update() { + const player_one_component = this.read_component(PlayerOneComponent); + + if (player_one_component) { + const player_one_entity = this.get_entity(player_one_component.entity_id); + + 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; + } + } + } + } +} +``` + +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 tremendous amount of flexibility to do what they need to do. + +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. + +{{% 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 %}} + +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. + +Let's add our InputEngine to the WorldBuilder. + +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!! diff --git a/content/pong/move_paddle/motion_engine.md b/content/pong/move_paddle/motion_engine.md new file mode 100644 index 0000000..6774998 --- /dev/null +++ b/content/pong/move_paddle/motion_engine.md @@ -0,0 +1,134 @@ +--- +title: "Motion Engine" +date: 2019-05-23T13:03:39-07:00 +weight: 5 +--- + +To create an Engine, we extend the Engine class. + +Create a file: **game/engines/motion.ts** + +```ts +import { Engine } from "encompass-ecs"; + +export class MotionEngine extends Engine { + public update(dt: number) {} +} +``` + +Every Engine needs an *update* method, which optionally 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. + +We're gonna need a Message. More specifically, a ComponentMessage. + +Create a file: **game/messages/component/motion.ts** + +```ts +import { ComponentMessage, Message } from "encompass-ecs"; +import { PositionComponent } from "game/components/position"; + +export class MotionMessage extends Message implements ComponentMessage { + public component: Readonly; + public x: number; + public y: number; +} +``` + +*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. + +{{% 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. +{{% /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"; + +@Reads(MotionMessage) +export class MotionEngine extends Engine { + public update(dt: number) { + const motion_messages = this.read_messages(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. + +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. + +So if we're going to be changing PositionComponents, the Engine needs to declare that it Mutates them, and then make the Component mutable. + +```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. + +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); + + ... + + } +``` + +Of course, if we run the game now, nothing will happen, because nothing is actually sending out MotionMessages. Let's make that happen. diff --git a/static/images/better.gif b/static/images/better.gif new file mode 100644 index 0000000..daa3170 Binary files /dev/null and b/static/images/better.gif differ diff --git a/static/images/favicon.png b/static/images/favicon.png new file mode 100644 index 0000000..a1cb51a Binary files /dev/null and b/static/images/favicon.png differ diff --git a/static/images/oh_dear.gif b/static/images/oh_dear.gif new file mode 100644 index 0000000..f7bb0ac Binary files /dev/null and b/static/images/oh_dear.gif differ