first section of pong tutorial
parent
d54c007bfc
commit
b110627a05
|
@ -0,0 +1,13 @@
|
||||||
|
+++
|
||||||
|
title = "Pong"
|
||||||
|
date = 2019-05-23T10:59:47-07:00
|
||||||
|
weight = 20
|
||||||
|
chapter = true
|
||||||
|
pre = "<b>4. </b>"
|
||||||
|
+++
|
||||||
|
|
||||||
|
### Chapter 4
|
||||||
|
|
||||||
|
# Pong
|
||||||
|
|
||||||
|
The game of games.
|
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
title: "Drawing A Paddle"
|
||||||
|
date: 2019-05-23T11:02:45-07:00
|
||||||
|
weight: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
It's nice to see something on screen right away when we start making a game, so let's make that happen.
|
||||||
|
|
||||||
|
In a 2D game, Encompass needs to know which order that things should draw in.
|
||||||
|
|
||||||
|
Encompass draws things back to front using integer layers. A negative value means farther in the back. A positive value means farther in the front. So an object on layer 10 will draw on top of an object on layer -10.
|
||||||
|
|
||||||
|
We'll need two things to get a paddle drawing on screen: A *DrawComponent* and an *EntityRenderer*.
|
||||||
|
|
||||||
|
Let's start with the Component.
|
|
@ -0,0 +1,37 @@
|
||||||
|
---
|
||||||
|
title: "Canvas Component"
|
||||||
|
date: 2019-05-23T11:26:31-07:00
|
||||||
|
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.
|
||||||
|
|
||||||
|
Create a file: **game/components/canvas.ts**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { DrawComponent } from "encompass-ecs";
|
||||||
|
|
||||||
|
export class CanvasComponent extends DrawComponent {
|
||||||
|
public canvas: Canvas;
|
||||||
|
public x_scale: number;
|
||||||
|
public y_scale: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's break this down a bit. What's a DrawComponent? A DrawComponent is a subtype of Component that includes a *layer* property, which is used for rendering.
|
||||||
|
|
||||||
|
*import* means that we are taking the definition of DrawComponent from another file, in this case the Encompass library. *export* means that we want this class to be available to other files in our project. If we don't export this class, it won't be very useful to us, so let's make sure to do that.
|
||||||
|
|
||||||
|
We provide some extra information, *x_scale* and *y_scale* so we can shrink or stretch the Canvas if we want to.
|
||||||
|
|
||||||
|
{{% notice notice %}}
|
||||||
|
You might be wondering - how does TypeScript know about things like Canvas, which are defined in LOVE? LOVE uses Lua, not TypeScript.
|
||||||
|
|
||||||
|
The answer is a thing called *definition files*. Definition files let TypeScript know about things that exist in the target environment. You don't really need to understand how it works just now, just know that the Encompass/LOVE starter pack depends on the lovely [love-typescript-definitions](https://github.com/hazzard993/love-typescript-definitions) project.
|
||||||
|
{{% /notice %}}
|
||||||
|
|
||||||
|
When we actually use the CanvasComponent, we will attach a Canvas that has stuff drawn on it. We'll get to that in a minute.
|
||||||
|
|
||||||
|
That's it for our CanvasComponent. We need one more bit of information before we can write our Renderer.
|
|
@ -0,0 +1,95 @@
|
||||||
|
---
|
||||||
|
title: "Canvas Renderer"
|
||||||
|
date: 2019-05-23T11:29:24-07:00
|
||||||
|
weight: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
Now that we have a CanvasComponent, we need to tell Encompass how to draw things that have it.
|
||||||
|
|
||||||
|
Create a file: **game/renderers/canvas.ts**
|
||||||
|
|
||||||
|
This is gonna be a bit more complex than our Components, so let's take this slowly.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Entity, EntityRenderer } from "encompass-ecs";
|
||||||
|
import { CanvasComponent } from "game/components/canvas";
|
||||||
|
import { PositionComponent } from "game/components/position";
|
||||||
|
|
||||||
|
export class CanvasRenderer extends EntityRenderer {
|
||||||
|
public component_types = [ PositionComponent ];
|
||||||
|
public draw_component_type = CanvasComponent;
|
||||||
|
|
||||||
|
public render(entity: Entity) {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
An *EntityRenderer* is defined by two properties and a method.
|
||||||
|
|
||||||
|
* It needs to have *component_types*, which is a list of Component types.
|
||||||
|
* It needs to specify a single *draw_component_type*.
|
||||||
|
* It needs to define a *render* method.
|
||||||
|
|
||||||
|
When an Entity has all of the Components listed in *component_types*, and a DrawComponent of *draw_component_type*, then it begins to *track* the Entity.
|
||||||
|
|
||||||
|
Each time *World.draw* is called, the EntityRenderer will run its *render* method on each Entity that it is tracking.
|
||||||
|
|
||||||
|
So, in our case, we want our CanvasRenderer to render any Entity that has a PositionComponent and a CanvasComponent. Simple as that.
|
||||||
|
|
||||||
|
Let's fill out our *render* method.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
public render(entity: Entity) {
|
||||||
|
const position_component = entity.get_component(PositionComponent);
|
||||||
|
const canvas_component = entity.get_component(CanvasComponent);
|
||||||
|
|
||||||
|
const canvas = canvas_component.canvas;
|
||||||
|
|
||||||
|
love.graphics.draw(
|
||||||
|
canvas,
|
||||||
|
position_component.x,
|
||||||
|
position_component.y,
|
||||||
|
0,
|
||||||
|
canvas_component.x_scale,
|
||||||
|
canvas_component.y_scale,
|
||||||
|
canvas.getWidth(),
|
||||||
|
canvas.getHeight(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
*Entity.get_component* is a method that gets a Component instance from an Entity when given a Component type. So when we say:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const position_component = entity.get_component(PositionComponent);
|
||||||
|
```
|
||||||
|
|
||||||
|
we are asking the Entity to give us access to its position information.
|
||||||
|
|
||||||
|
Once we have our specific position and canvas information, we can use that information to tell LOVE to draw something!
|
||||||
|
|
||||||
|
```ts
|
||||||
|
love.graphics.draw(
|
||||||
|
canvas,
|
||||||
|
position_component.x,
|
||||||
|
position_component.y,
|
||||||
|
0,
|
||||||
|
canvas_component.x_scale,
|
||||||
|
canvas_component.y_scale,
|
||||||
|
canvas.getWidth() * 0.5,
|
||||||
|
canvas.getHeight() * 0.5,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
This is simply a call to the *love.graphics.draw* function that LOVE provides. You can read more about it [here](https://love2d.org/wiki/love.graphics.draw). We are just telling LOVE to draw our canvas at our PositionComponent's position, with 0 rotation, our scaling factor, and an offset of the canvas's width and height divided by 2. The offset just tells LOVE to draw the canvas starting at the center of the canvas, instead of at the top left corner.
|
||||||
|
|
||||||
|
That's it! Now we need to set up our World with its starting configuration so our Encompass elements can work in concert.
|
||||||
|
|
||||||
|
{{% notice notice %}}
|
||||||
|
Clever readers may have noticed something here. Aren't Entities allowed to have any number of Components of a given type? So why is *get_component* singular?
|
||||||
|
|
||||||
|
We actually have two different component getter methods: *Entity.get_component*, and *Entity.get_components*, which will return a list of all the components of the given type on the Entity.
|
||||||
|
|
||||||
|
In this case, I am assuming that an Entity will only ever have one PositionComponent, so I am using the *get_component* method for convenience.
|
||||||
|
|
||||||
|
You are allowed to make any assumptions about the structure of your Entities as you want - just make sure your assumptions stay consistent, or you will have unpleasant surprises!
|
||||||
|
{{% /notice %}}
|
|
@ -0,0 +1,35 @@
|
||||||
|
---
|
||||||
|
title: "First Run"
|
||||||
|
date: 2019-05-23T12:21:05-07:00
|
||||||
|
weight: 20
|
||||||
|
---
|
||||||
|
|
||||||
|
All we have to do now is run our build and run script in the terminal.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
> npm run love
|
||||||
|
```
|
||||||
|
|
||||||
|
Exciting!! Let's see what happens...
|
||||||
|
|
||||||
|
![pong first run](/images/pong_first_run.png)
|
||||||
|
|
||||||
|
Oh dear. That paddle is quite small. Bit of a buzzkill really.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const width = 20;
|
||||||
|
const height = 120;
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
position_component.x = 40;
|
||||||
|
position_component.y = 360;
|
||||||
|
```
|
||||||
|
|
||||||
|
![pong second run](/images/pong_second_run.png)
|
||||||
|
|
||||||
|
Thaaaaaaat's more like it.
|
||||||
|
|
||||||
|
Notice how we can just change simple Component values, and the result of the simulation changes. In a larger project we would probably want to define these Component values in a separate file that lives on its own. This is called *data-driven design* and it is a powerful feature of ECS-like architectures. When we do data-driven design, we can modify the game without even looking at source code - just change some values in a file and the game changes! If we wanted to get really clever, we could probably have an in-game editor that changes these values even while the game is running!
|
||||||
|
|
||||||
|
But for such a simple example, leaving this in the *load* function is probably fine. Let's move on and get this paddle moving.
|
|
@ -0,0 +1,159 @@
|
||||||
|
---
|
||||||
|
title: "Initializing the World"
|
||||||
|
date: 2019-05-23T12:06:18-07:00
|
||||||
|
weight: 15
|
||||||
|
---
|
||||||
|
|
||||||
|
It's time to put it all together.
|
||||||
|
|
||||||
|
Let's look at our **game/game.ts** file. The *load* method looks like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
public load() {
|
||||||
|
this.canvas = love.graphics.newCanvas();
|
||||||
|
|
||||||
|
const world_builder = new WorldBuilder();
|
||||||
|
|
||||||
|
// ADD YOUR ENGINES HERE...
|
||||||
|
|
||||||
|
// ADD YOUR RENDERERS HERE...
|
||||||
|
|
||||||
|
// ADD YOUR STARTING ENTITIES HERE...
|
||||||
|
|
||||||
|
this.world = world_builder.build();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's do as the helpful file asks, eh?
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { CanvasRenderer } from "./renderers/canvas";
|
||||||
|
...
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
...
|
||||||
|
|
||||||
|
public load() {
|
||||||
|
this.canvas = love.graphics.newCanvas();
|
||||||
|
|
||||||
|
const world_builder = new WorldBuilder();
|
||||||
|
|
||||||
|
// ADD YOUR ENGINES HERE...
|
||||||
|
|
||||||
|
// ADD YOUR RENDERERS HERE...
|
||||||
|
world_builder.add_renderer(CanvasRenderer);
|
||||||
|
|
||||||
|
// ADD YOUR STARTING ENTITIES HERE...
|
||||||
|
|
||||||
|
this.world = world_builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Now our CanvasRenderer will exist in the world. We only have two things left to do: create a Canvas that contains our paddle visuals, and put it on an Entity.
|
||||||
|
|
||||||
|
Let's tell the World Builder that we want a new Entity. This will be our paddle Entity.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const paddle_entity = world_builder.create_entity();
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's set up our paddle Canvas.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const width = 4;
|
||||||
|
const height = 8;
|
||||||
|
|
||||||
|
const paddle_canvas = love.graphics.newCanvas(4, 8);
|
||||||
|
love.graphics.setCanvas(paddle_canvas);
|
||||||
|
love.graphics.setBlendMode("alpha");
|
||||||
|
love.graphics.setColor(1, 1, 1, 1);
|
||||||
|
love.graphics.rectangle("fill", 0, 0, length, 2);
|
||||||
|
love.graphics.setCanvas();
|
||||||
|
```
|
||||||
|
|
||||||
|
All we're doing here is setting up a Canvas and filling it with a white rectangle. If you want to break this down more, go ahead and read the [love.graphics documentation](https://love2d.org/wiki/love.graphics).
|
||||||
|
|
||||||
|
Now we need to attach the canvas to the CanvasComponent.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const canvas_component = paddle_entity.add_component(CanvasComponent);
|
||||||
|
canvas_component.canvas = paddle_canvas;
|
||||||
|
canvas_component.x_scale = 1;
|
||||||
|
canvas_component.y_scale = 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, let's set up its position.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const position_component = paddle_entity.add_component(PositionComponent);
|
||||||
|
position_component.x = 40;
|
||||||
|
position_component.y = 40;
|
||||||
|
```
|
||||||
|
|
||||||
|
Our final **game/game.ts** should look like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { World, WorldBuilder } from "encompass-ecs";
|
||||||
|
import { CanvasComponent } from "./components/canvas";
|
||||||
|
import { PositionComponent } from "./components/position";
|
||||||
|
import { CanvasRenderer } from "./renderers/canvas";
|
||||||
|
|
||||||
|
export class Game {
|
||||||
|
private world: World;
|
||||||
|
private canvas: Canvas;
|
||||||
|
|
||||||
|
public load() {
|
||||||
|
this.canvas = love.graphics.newCanvas();
|
||||||
|
|
||||||
|
const world_builder = new WorldBuilder();
|
||||||
|
|
||||||
|
// ADD YOUR ENGINES HERE...
|
||||||
|
|
||||||
|
// ADD YOUR RENDERERS HERE...
|
||||||
|
world_builder.add_renderer(CanvasRenderer);
|
||||||
|
|
||||||
|
// ADD YOUR STARTING ENTITIES HERE...
|
||||||
|
const paddle_entity = world_builder.create_entity();
|
||||||
|
|
||||||
|
const width = 4;
|
||||||
|
const height = 8;
|
||||||
|
|
||||||
|
const paddle_canvas = love.graphics.newCanvas(width, height);
|
||||||
|
love.graphics.setCanvas(paddle_canvas);
|
||||||
|
love.graphics.setBlendMode("alpha");
|
||||||
|
love.graphics.setColor(1, 1, 1, 1);
|
||||||
|
love.graphics.rectangle("fill", 0, 0, width, height);
|
||||||
|
love.graphics.setCanvas();
|
||||||
|
|
||||||
|
const canvas_component = paddle_entity.add_component(CanvasComponent);
|
||||||
|
canvas_component.canvas = paddle_canvas;
|
||||||
|
canvas_component.x_scale = 1;
|
||||||
|
canvas_component.y_scale = 1;
|
||||||
|
|
||||||
|
const position_component = paddle_entity.add_component(PositionComponent);
|
||||||
|
position_component.x = 40;
|
||||||
|
position_component.y = 40;
|
||||||
|
|
||||||
|
this.world = world_builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(dt: number) {
|
||||||
|
this.world.update(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public draw() {
|
||||||
|
love.graphics.clear();
|
||||||
|
love.graphics.setCanvas(this.canvas);
|
||||||
|
love.graphics.clear();
|
||||||
|
this.world.draw();
|
||||||
|
love.graphics.setCanvas();
|
||||||
|
love.graphics.setBlendMode("alpha", "premultiplied");
|
||||||
|
love.graphics.setColor(1, 1, 1, 1);
|
||||||
|
love.graphics.draw(this.canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let's run the game!!
|
|
@ -0,0 +1,22 @@
|
||||||
|
---
|
||||||
|
title: "Position Component"
|
||||||
|
date: 2019-05-23T11:34:58-07:00
|
||||||
|
weight: 10
|
||||||
|
---
|
||||||
|
|
||||||
|
This one is pretty simple. We can't draw something if we don't know *where* on screen to draw it.
|
||||||
|
|
||||||
|
Well, why didn't we put that in the CanvasComponent? The reason is that position is a concept that is relevant in more situations than just drawing. For example: collision, yeah? So it really needs to be its own component.
|
||||||
|
|
||||||
|
Create a file: **game/components/position.ts**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Component } from "encompass-ecs";
|
||||||
|
|
||||||
|
export class PositionComponent extends Component {
|
||||||
|
public x: number;
|
||||||
|
public y: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Notice that we haven't created a file that is more than 10 lines long yet. I hope you're starting to notice the power of modularity here.
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
title: "Intro"
|
||||||
|
date: 2019-05-23T11:03:45-07:00
|
||||||
|
weight: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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!
|
|
@ -16,7 +16,7 @@ Unfortunately, things aren't quite this simple when it comes to more complex gam
|
||||||
|
|
||||||
As programmers we want to re-use code as much as possible. Every bit of duplication is an opportunity for bugs to lurk in our program. Object-oriented code accomplishes re-use with a concept called *inheritance*. With inheritance, classes can be partially based on other classes. Maybe a Ball class has a position and a velocity and a bounciness property. A BouncyBall would inherit from Ball and have a greater value in its bounciness property. Simple enough, right?
|
As programmers we want to re-use code as much as possible. Every bit of duplication is an opportunity for bugs to lurk in our program. Object-oriented code accomplishes re-use with a concept called *inheritance*. With inheritance, classes can be partially based on other classes. Maybe a Ball class has a position and a velocity and a bounciness property. A BouncyBall would inherit from Ball and have a greater value in its bounciness property. Simple enough, right?
|
||||||
|
|
||||||
But we soon run into problems. In game development we often wish to mix-and-match behaviors. Suppose I have an object where it would make sense to inherit from *two* different classes. Now... we are hosed! Why? If two parent classes have a property or a method with the same name, now our child object has no idea what to do. Object-oriented systems, in fact, forbid multiple inheritance. So we end up having to share code via helper functions, or giant manager classes, or other awkward patterns.
|
But we soon run into problems. In game development we often wish to mix-and-match behaviors. Suppose I have an object where it would make sense to inherit from *two* different classes. Now... we are hosed! Why? If two parent classes have a property or a method with the same name, now our child object has no idea what to do. Most object-oriented systems, in fact, forbid multiple inheritance, and the ones that don't forbid it require very complex definitions to make it work. So we end up having to share code via helper functions, or giant manager classes, or other awkward patterns.
|
||||||
|
|
||||||
We also run into an issue called *tight coupling*. Objects that reference each other's properties or methods directly become a problem when we change the structure of those objects in any way. If we modify the structure of object B, and object A references object B, then we have to also modify object A. In a particularly poorly structured system, we might have to modify a dozen objects just to make a slight modification to the behavior of a single object.
|
We also run into an issue called *tight coupling*. Objects that reference each other's properties or methods directly become a problem when we change the structure of those objects in any way. If we modify the structure of object B, and object A references object B, then we have to also modify object A. In a particularly poorly structured system, we might have to modify a dozen objects just to make a slight modification to the behavior of a single object.
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
Loading…
Reference in New Issue