rewrite the paddle drawing section
parent
9bc6934de0
commit
f3e07780e3
|
@ -3,6 +3,8 @@ publishDir = "public"
|
||||||
languageCode = "en-US"
|
languageCode = "en-US"
|
||||||
title = "Encompass Docs"
|
title = "Encompass Docs"
|
||||||
theme = "hugo-theme-learn"
|
theme = "hugo-theme-learn"
|
||||||
|
canonifyUrls = true
|
||||||
|
relativeUrls = true
|
||||||
|
|
||||||
[outputs]
|
[outputs]
|
||||||
home = [ "HTML", "RSS", "JSON" ]
|
home = [ "HTML", "RSS", "JSON" ]
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
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. To create a new Component type, we extend the Component class.
|
|
||||||
|
|
||||||
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.
|
|
|
@ -4,90 +4,151 @@ date: 2019-05-23T11:29:24-07:00
|
||||||
weight: 10
|
weight: 10
|
||||||
---
|
---
|
||||||
|
|
||||||
Now that we have a CanvasComponent, we need to tell Encompass how to draw things that have it.
|
Now that we have a Texture2DComponent and a PositionComponent, we can tell Encompass how to draw things that have them.
|
||||||
|
|
||||||
Create a file: **game/renderers/canvas.ts**
|
Create a file: **PongFE/Renderers/Texture2DRenderer.cs**
|
||||||
|
|
||||||
This is gonna be a bit more complex than our Components, so let's take this slowly.
|
This is gonna be a bit more complex than our Components, so let's take this slowly.
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
import { Entity, EntityRenderer } from "encompass-ecs";
|
using Encompass;
|
||||||
import { CanvasComponent } from "game/components/canvas";
|
using PongFE.Components;
|
||||||
import { PositionComponent } from "game/components/position";
|
|
||||||
|
|
||||||
@Renders(CanvasComponent, PositionComponent)
|
namespace PongFE.Renderers
|
||||||
export class CanvasRenderer extends EntityRenderer {
|
{
|
||||||
public render(entity: Entity) {}
|
public class Texture2DRenderer : OrderedRenderer<Texture2DComponent>
|
||||||
|
{
|
||||||
|
public override void Render(Entity entity, in Texture2DComponent drawComponent)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
An *EntityRenderer* is defined by the Components it tracks and a *render* method.
|
Before we go any further, let's talk about some of the new concepts introduced here.
|
||||||
|
|
||||||
**@Renders** is a function called a *class decorator*. Its first argument should be the DrawComponent, and the subsequent arguments are any number of components that are also required for the EntityRenderer to *track* an Entity.
|
Notice that this is a *class* instead of a struct, like we have done before. There are many reasons why you might want to use classes vs structs in C# in general, but for our purposes, you can use the following rule of thumb: if the object contains data, it should be a struct. If it contains logic, it should be a class. Since Renderers draw things (logic), they are classes.
|
||||||
|
|
||||||
{{% notice tip %}}
|
An *OrderedRenderer* is defined by the Component type it tracks and a *Render* method. It uses the *Layer* property of the specified Component to draw things in the correct order. Each time *World.Draw* is called, the OrderedRenderer will run its *Render* method for each Entity that contains a Component of its specified tracking type.
|
||||||
You can read more about decorators on the [official TypeScript documentation](https://www.typescriptlang.org/docs/handbook/decorators.html).
|
|
||||||
|
The most efficient way for us to draw Texture2Ds is to use FNA's SpriteBatch system. If you actually care about how SpriteBatch works and why we need to structure our draws using it, read the Note section below. Otherwise feel free to just skip ahead.
|
||||||
|
|
||||||
|
{{% notice note %}}
|
||||||
|
Why SpriteBatch? Modern computers have dedicated chips for processing graphics-related logic. These are called GPUs, or Graphics Processing Units. In order to draw using the GPU, the CPU (central processing unit) must send data to the GPU.
|
||||||
|
|
||||||
|
Imagine that you want to bake cookies. One way you could do this is by putting a single blob of cookie dough on a sheet, stick it in the oven, wait 15 or so minutes for it to bake, take it out, and then repeat until all your cookies are done. Or... you could stick a dozen cookies on a single baking sheet and bake them all at the same time. It should be pretty obvious which one of these methods is faster.
|
||||||
|
|
||||||
|
If you understand the analogy you will see why sending Texture2Ds to the GPU to be drawn one at a time will bog down the CPU enormously. So what we want to do is *batch* the data. This is what SpriteBatch does!
|
||||||
{{% /notice %}}
|
{{% /notice %}}
|
||||||
|
|
||||||
Each time *World.draw* is called, the EntityRenderer will run its *render* method on each Entity that it is tracking.
|
Now let's fill out the Renderer some more.
|
||||||
|
|
||||||
So, in our case, we want our CanvasRenderer to render any Entity that has a PositionComponent and a CanvasComponent. Simple as that.
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using PongFE.Components;
|
||||||
|
|
||||||
Let's fill out our *render* method.
|
namespace PongFE.Renderers
|
||||||
|
{
|
||||||
|
public class Texture2DRenderer : OrderedRenderer<Texture2DComponent>
|
||||||
|
{
|
||||||
|
private readonly SpriteBatch _spriteBatch;
|
||||||
|
|
||||||
```ts
|
public Texture2DRenderer(SpriteBatch spriteBatch)
|
||||||
public render(entity: Entity) {
|
{
|
||||||
const position_component = entity.get_component(PositionComponent);
|
_spriteBatch = spriteBatch;
|
||||||
const canvas_component = entity.get_component(CanvasComponent);
|
}
|
||||||
|
|
||||||
const canvas = canvas_component.canvas;
|
public override void Render(Entity entity, in Texture2DComponent textureComponent)
|
||||||
|
{
|
||||||
|
ref readonly var positionComponent = ref GetComponent<PositionComponent>(entity);
|
||||||
|
|
||||||
love.graphics.draw(
|
_spriteBatch.Draw(
|
||||||
canvas,
|
textureComponent.Texture,
|
||||||
position_component.x,
|
positionComponent.Position,
|
||||||
position_component.y,
|
null,
|
||||||
|
Color.White,
|
||||||
0,
|
0,
|
||||||
canvas_component.x_scale,
|
Vector2.Zero,
|
||||||
canvas_component.y_scale,
|
Vector2.One,
|
||||||
canvas.getWidth() * 0.5,
|
SpriteEffects.None,
|
||||||
canvas.getHeight() * 0.5,
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
*Entity.get_component* is a method that gets a Component instance from an Entity when given a Component type. So when we say:
|
First of all, when we construct the Texture2DRenderer, we will pass in a SpriteBatch instance. Simple enough.
|
||||||
|
|
||||||
```ts
|
Our *Render* method will run exactly once for each Texture2DComponent that lives in our world. It also gives us a reference to the Entity that each specific Texture2DComponent is attached to. The *in* keyword means that the textureComponent is accessed by reference, but cannot be modified. We shouldn't ever be changing the data of a component inside a Renderer, because that is not the Renderer's job. So the method requires us to use the *in* keyword here.
|
||||||
const position_component = entity.get_component(PositionComponent);
|
|
||||||
|
Next, we want to retrieve our position data. We can do this with the Renderer's `GetComponent` method.
|
||||||
|
|
||||||
|
`GetComponent<PositionComponent>(entity)` means that we retrieve the PositionComponent that is attached to the given entity. Simple! The brackets mean that the method is what is called a *generic method*. That means we have to tell the method which type it needs to return, which in this case is PositionComponent.
|
||||||
|
|
||||||
|
*ref* means that we are *referencing* the component struct rather than copying it by value. This is optional, but it is *much, much* faster than not using *ref* so I recommend doing this pretty much always.
|
||||||
|
|
||||||
|
*readonly* is similar to *in* because it means that the struct's values cannot be modified. We shouldn't ever be changing the data of a component inside a Renderer, and in fact, `GetComponent` does not even allow you to use non-readonly references. We will talk more about strategies for updating components later on.
|
||||||
|
|
||||||
|
Now that we can access our data, we have a problem though. We are using a custom struct for handling our Position data, but the SpriteBatch doesn't know how to use that data. It only knows how to use a `Microsoft.Xna.Framework.Vector2` for position. So what do we do?
|
||||||
|
|
||||||
|
We have two options in this case: we could create a helper function that takes in a `MoonTools.Structs.Position2D` position struct and converts it to an appropriate `Microsoft.Xna.Framework.Vector2` struct. But in cases like these, I think it is cleaner to define what we call an *extension method*.
|
||||||
|
|
||||||
|
Let's create a folder in our PongFE directory called Extensions. Inside of Extensions, create a file: **PongFE/Extensions/Position2DExtensions.cs**
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using MoonTools.Structs;
|
||||||
|
|
||||||
|
namespace PongFE.Extensions
|
||||||
|
{
|
||||||
|
public static class Position2DExtensions
|
||||||
|
{
|
||||||
|
public static Microsoft.Xna.Framework.Vector2 ToXNAVector(this Position2D position)
|
||||||
|
{
|
||||||
|
return new Microsoft.Xna.Framework.Vector2(position.X, position.Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
we are asking the Entity to give us access to its position information.
|
When we define a method in this way, it means that we can add a new method to an existing class even if we don't have control over the class's implementation. This can be a very nice and clean way to add functionality that we need for our project without having to modify the original code. If you find yourself using tons and tons of extension methods, however, it could be time to consider creating your own class instead.
|
||||||
|
|
||||||
Once we have our specific position and canvas information, we can use that information to tell LOVE to draw something!
|
Now we can rewrite our SpriteBatch draw call:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
love.graphics.draw(
|
_spriteBatch.Draw(
|
||||||
canvas,
|
textureComponent.Texture,
|
||||||
position_component.x,
|
positionComponent.Position.ToXNAVector(),
|
||||||
position_component.y,
|
null,
|
||||||
|
Color.White,
|
||||||
0,
|
0,
|
||||||
canvas_component.x_scale,
|
Vector2.Zero,
|
||||||
canvas_component.y_scale,
|
Vector2.One,
|
||||||
canvas.getWidth() * 0.5,
|
SpriteEffects.None,
|
||||||
canvas.getHeight() * 0.5,
|
0
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
Let's take this opportunity to break down the `SpriteBatch.Draw` method. There are actually a few different configurations of Draw method arguments, but this is the most commonly used one.
|
||||||
|
|
||||||
|
The first argument is the texture we are going to draw.
|
||||||
|
|
||||||
|
The second argument is the position where we will draw the texture.
|
||||||
|
|
||||||
|
The third argument is the "source rectangle" of the texture. This can be useful when we use spritesheets, which let us pack lots of sprites into the same texture, increasing efficiency. We don't need it right now, so we can just pass `null` and the sprite batch assumes we are drawing the entire texture.
|
||||||
|
|
||||||
|
The fourth argument is a color blend value. We don't want to change the color of the sprite, so `Color.White` will draw the sprite normally.
|
||||||
|
|
||||||
|
The fifth argument is a rotation value. We aren't rotating anything right now so we can just use 0.
|
||||||
|
|
||||||
|
The sixth argument is an "origin" value. All transformations of a sprite, like rotation and scaling, take place relative to the origin. For example, if you wanted the sprite to rotate around its center, we could pass in the center point of the sprite as the origin. We can ignore this for now, so we just use `Vector2.Zero`.
|
||||||
|
|
||||||
|
The seventh argument is a scaling value, which multiples the sprite's dimensions by the given value. For example, if we wanted to make the sprite draw twice as large, we could pass `new Vector(2, 2)` here. We just want to draw the sprite at its original dimensions, so let's just use `Vector2.One` here.
|
||||||
|
|
||||||
|
The eighth argument is a `SpriteEffects` argument, which can be used to flip the sprite horizontally or vertically. This argument is basically useless and you will pretty much never need to pass anything except `SpriteEffects.None` here, because passing in negative scaling values can handle sprite flipping. This is one of those examples of the XNA design being a bit weird and crusty in certain places.
|
||||||
|
|
||||||
|
The ninth and final argument is a `layerDepth` integer. This is only used when the SpriteBatch uses an internal sorting technique. This is much less efficient than letting Encompass do the sorting, so we will ignore this value and just pass 0 for everything.
|
||||||
|
|
||||||
That's it! Now we need to set up our World with its starting configuration so our Encompass elements can work in concert.
|
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 %}}
|
|
||||||
|
|
|
@ -4,32 +4,44 @@ date: 2019-05-23T12:21:05-07:00
|
||||||
weight: 20
|
weight: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
All we have to do now is run our build and run script in the terminal.
|
If you are using VSCode, all we have to do now is run our build and run task.
|
||||||
|
|
||||||
```sh
|
Hit `Ctrl-Shift-B` (or `Cmd-Shift-B` on OSX) to bring up the Build Tasks window.
|
||||||
> npm run love
|
|
||||||
```
|
![Build Tasks window](/images/build_tasks.png)
|
||||||
|
|
||||||
|
There's a lot of options here so let's break it down a bit.
|
||||||
|
|
||||||
|
`Build` tasks simply build the program, but do not execute it.
|
||||||
|
`Run` tasks execute without building.
|
||||||
|
`Build & Run` tasks build and then execute.
|
||||||
|
|
||||||
|
`Framework` means that the game will execute using .NET Framework (or Mono if you are on OSX or Linux).
|
||||||
|
`Core` means that the game will execute using .NET Core.
|
||||||
|
|
||||||
|
FNA recommends developing and shipping using Framework/Mono, but I find the Core debugger to be pretty powerful, so I switch to Core sometimes when I have a tricky bug to track down.
|
||||||
|
|
||||||
|
`Debug` means that the game will build in Debug mode.
|
||||||
|
`Release` means that the game will build in Release mode.
|
||||||
|
Debug modes are typically larger and slower than `Release` builds, but they allow you to use powerful debugging tools.
|
||||||
|
When you are shipping your game you should always, always use `Release` mode.
|
||||||
|
|
||||||
|
Anyway, for our first run, let's select `Build & Run: Framework Debug`.
|
||||||
|
|
||||||
Exciting!! Let's see what happens...
|
Exciting!! Let's see what happens...
|
||||||
|
|
||||||
![pong first run](/images/pong_first_run.png)
|
![pong first run](/images/pong_first_run.png)
|
||||||
|
|
||||||
Oh dear. That paddle is quite small. Bit of a buzzkill really.
|
Look at that! Terrific.
|
||||||
|
|
||||||
```ts
|
Cornflower blue is a lovely color, but for authenticity we probably want the background to be black instead.
|
||||||
const width = 20;
|
|
||||||
const height = 120;
|
|
||||||
```
|
|
||||||
|
|
||||||
```ts
|
In the Draw method, change Color.CornflowerBlue to Color.Black:
|
||||||
position_component.x = 40;
|
|
||||||
position_component.y = 360;
|
```cs
|
||||||
|
GraphicsDevice.Clear(Color.Black);
|
||||||
```
|
```
|
||||||
|
|
||||||
![pong second run](/images/pong_second_run.png)
|
![pong second run](/images/pong_second_run.png)
|
||||||
|
|
||||||
Thaaaaaaat's more like it.
|
Thaaaaaaat's more like it. But it's pretty boring right now. Games need action! Let's move on and get this paddle moving.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
|
@ -6,152 +6,192 @@ weight: 15
|
||||||
|
|
||||||
It's time to put it all together.
|
It's time to put it all together.
|
||||||
|
|
||||||
Let's look at our **game/game.ts** file. The *load* method looks like this:
|
Let's look at our **PongFE/PongFEGame.cs** file. The *LoadContent* method looks like this:
|
||||||
|
|
||||||
```ts
|
```cs
|
||||||
public load() {
|
protected override void LoadContent()
|
||||||
this.canvas = love.graphics.newCanvas();
|
{
|
||||||
|
World = WorldBuilder.Build();
|
||||||
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?
|
In the beginning, the World is without form, and void; and darkness is upon the face of the deep. Let's fix that!
|
||||||
|
|
||||||
```ts
|
Obviously, if we want to draw a Pong paddle, we will need a Texture2D that represents the paddle. A Pong paddle is just a white rectangle. One approach we could take is open up an image editor, draw a white rectangle, save it to a PNG file, and then tell our game to load it. That sounds annoying though - what if we want to tweak the dimensions slightly? We have to repeat the entire process. That's a lot of work just to draw a silly rectangle.
|
||||||
import { CanvasRenderer } from "./renderers/canvas";
|
|
||||||
...
|
|
||||||
|
|
||||||
export class Game {
|
Let's define our Pong paddle programmatically instead.
|
||||||
...
|
|
||||||
|
|
||||||
public load() {
|
First things first - at the top of the PongFEGame class, let's define a SpriteBatch instance and a Texture2D instance.
|
||||||
this.canvas = love.graphics.newCanvas();
|
|
||||||
|
|
||||||
const world_builder = new WorldBuilder();
|
```cs
|
||||||
|
WorldBuilder WorldBuilder { get; } = new WorldBuilder();
|
||||||
|
World World { get; set; }
|
||||||
|
|
||||||
// ADD YOUR ENGINES HERE...
|
SpriteBatch SpriteBatch { get; set; }
|
||||||
|
Texture2D WhitePixel { get; set; }
|
||||||
|
RenderTarget2D PaddleTexture { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
// ADD YOUR RENDERERS HERE...
|
We haven't seen `RenderTarget2D` yet. Basically, a render target is a special kind of texture that we can draw to *instead* of drawing directly to the screen. These can come in extremely handy for many use cases, like generating textures at runtime and creating fullscreen post-processing effects.
|
||||||
world_builder.add_renderer(CanvasRenderer);
|
|
||||||
|
|
||||||
// ADD YOUR STARTING ENTITIES HERE...
|
Now, at the top of the LoadContent method, add the following lines:
|
||||||
|
|
||||||
this.world = world_builder.build();
|
```cs
|
||||||
}
|
protected override void LoadContent()
|
||||||
|
{
|
||||||
|
SpriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
|
||||||
|
WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
|
||||||
|
WhitePixel.SetData(new Color[] { Color.White });
|
||||||
|
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
*GraphicsDevice* is an instance attached to the Game class. It basically represents the handle that our program has to the device on our computer that will be handling the draw calls. All draw-related operations will be going through the GraphicsDevice. GraphicsDevice is guaranteed to be initialized by the time LoadContent is called, so we will be doing our graphics-related initialization work in there.
|
||||||
|
|
||||||
Let's tell the World Builder that we want a new Entity. This will be our paddle Entity.
|
First, we create a SpriteBatch instance. We can re-use the same SpriteBatch instance for each batch we need, so we'll just create one of these and re-use it throughout our game.
|
||||||
|
|
||||||
```ts
|
Next, we create a new 1x1 Texture. `Texture2D.SetData` lets us set the color values of each pixel in the Texture. We just want a single white pixel.
|
||||||
const paddle_entity = world_builder.create_entity();
|
|
||||||
|
Now, we can use our pixel to draw a rectangle of any size we want. Check this out;
|
||||||
|
|
||||||
|
```cs
|
||||||
|
PaddleTexture = new RenderTarget2D(GraphicsDevice, 20, 80);
|
||||||
|
GraphicsDevice.SetRenderTarget(PaddleTexture);
|
||||||
|
SpriteBatch.Begin();
|
||||||
|
SpriteBatch.Draw(WhitePixel, new Rectangle(0, 0, 20, 80), Color.White);
|
||||||
|
SpriteBatch.End();
|
||||||
|
GraphicsDevice.SetRenderTarget(null);
|
||||||
```
|
```
|
||||||
|
|
||||||
Let's set up our paddle Canvas.
|
First, we instantiate our PaddleTexture render target. Then, we set the current render target to PaddleTexture. This means that all draw calls will now draw to this texture instead of to the screen. Next, we begin the SpriteBatch, draw our white pixel to a rectangle of size 20x80, and end the SpriteBatch. Finally, setting the current render target back to `null` means that draw calls will go to the screen.
|
||||||
|
|
||||||
```ts
|
Now we have a Pong paddle of size 20x80! All that remains is to attach it to an Entity.
|
||||||
const width = 4;
|
|
||||||
const height = 8;
|
|
||||||
|
|
||||||
const paddle_canvas = love.graphics.newCanvas(4, 8);
|
```cs
|
||||||
love.graphics.setCanvas(paddle_canvas);
|
var paddle = WorldBuilder.CreateEntity();
|
||||||
love.graphics.setBlendMode("alpha");
|
WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
|
||||||
love.graphics.setColor(1, 1, 1, 1);
|
WorldBuilder.SetComponent(paddle, new Texture2DComponent(PaddleTexture, 0));
|
||||||
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).
|
WorldBuilder is the class we use to set up the starting state of our World.
|
||||||
|
|
||||||
Now we need to attach the canvas to the CanvasComponent.
|
First, we tell WorldBuilder to create a new Entity. This creates an empty Entity with no components.
|
||||||
|
|
||||||
```ts
|
Next, we create a new PositionComponent and tell the WorldBuilder to attach it to the paddle entity by calling SetComponent.
|
||||||
const canvas_component = paddle_entity.add_component(CanvasComponent);
|
|
||||||
canvas_component.canvas = paddle_canvas;
|
Finally, we create a new Texture2DComponent using our PaddleTexture, and put it on layer 0. This is attached to the paddle entity with SetComponent as well.
|
||||||
canvas_component.x_scale = 1;
|
|
||||||
canvas_component.y_scale = 1;
|
Now we want to attach our Texture2DRenderer to the World.
|
||||||
|
|
||||||
|
```cs
|
||||||
|
...
|
||||||
|
WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));
|
||||||
|
|
||||||
|
var paddle = WorldBuilder.CreateEntity();
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, let's set up its position.
|
We set up our Renderers before we create any entities and components. We give our SpriteBatch instance to the new Texture2DRenderer. WorldBuilder.AddOrderedRenderer tells the World to use this Renderer. That's it!
|
||||||
|
|
||||||
```ts
|
We have one little bit of housekeeping to take care of before we can run the game. SpriteBatch can only draw after Begin has been called, and it must call End before anything will draw to the screen. Recall that our Texture2DRenderer does not call Begin or End, because that would only batch one Texture2D at a time, which is very inefficient.
|
||||||
const position_component = paddle_entity.add_component(PositionComponent);
|
|
||||||
position_component.x = 40;
|
Our game's Draw method should look like this:
|
||||||
position_component.y = 40;
|
|
||||||
|
```cs
|
||||||
|
protected override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
GraphicsDevice.Clear(Color.CornflowerBlue);
|
||||||
|
|
||||||
|
SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
|
||||||
|
World.Draw();
|
||||||
|
SpriteBatch.End();
|
||||||
|
|
||||||
|
base.Draw(gameTime);
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Our final **game/game.ts** should look like this:
|
SpriteSortMode.Deferred is the most efficient SpriteBatch drawing mode. It waits as long as possible to send data to the GPU. BlendState.NonPremultiplied is a "blend mode". We will talk more about these later on, but feel free to read about them on your own. There are better ways we might need to structure our SpriteBatch Begins and Ends, but this will do just fine for now.
|
||||||
|
|
||||||
```ts
|
Our final PongFEGame should look like this:
|
||||||
import { World, WorldBuilder } from "encompass-ecs";
|
|
||||||
import { CanvasComponent } from "./components/canvas";
|
|
||||||
import { PositionComponent } from "./components/position";
|
|
||||||
import { CanvasRenderer } from "./renderers/canvas";
|
|
||||||
|
|
||||||
export class Game {
|
```cs
|
||||||
private world: World;
|
using Encompass;
|
||||||
private canvas: Canvas;
|
using Microsoft.Xna.Framework;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
using PongFE.Components;
|
||||||
|
using PongFE.Renderers;
|
||||||
|
|
||||||
public load() {
|
namespace PongFE
|
||||||
this.canvas = love.graphics.newCanvas();
|
{
|
||||||
|
class PongFEGame : Game
|
||||||
|
{
|
||||||
|
GraphicsDeviceManager graphics;
|
||||||
|
|
||||||
const world_builder = new WorldBuilder();
|
WorldBuilder WorldBuilder { get; } = new WorldBuilder();
|
||||||
|
World World { get; set; }
|
||||||
|
|
||||||
// ADD YOUR ENGINES HERE...
|
SpriteBatch SpriteBatch { get; set; }
|
||||||
|
Texture2D WhitePixel { get; set; }
|
||||||
|
RenderTarget2D PaddleTexture { get; set; }
|
||||||
|
|
||||||
// ADD YOUR RENDERERS HERE...
|
public PongFEGame()
|
||||||
world_builder.add_renderer(CanvasRenderer);
|
{
|
||||||
|
graphics = new GraphicsDeviceManager(this);
|
||||||
|
graphics.PreferredBackBufferWidth = 1280;
|
||||||
|
graphics.PreferredBackBufferHeight = 720;
|
||||||
|
graphics.PreferMultiSampling = true;
|
||||||
|
Content.RootDirectory = "Content";
|
||||||
|
|
||||||
// ADD YOUR STARTING ENTITIES HERE...
|
Window.AllowUserResizing = true;
|
||||||
const paddle_entity = world_builder.create_entity();
|
IsMouseVisible = true;
|
||||||
|
|
||||||
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) {
|
protected override void LoadContent()
|
||||||
this.world.update(dt);
|
{
|
||||||
|
SpriteBatch = new SpriteBatch(GraphicsDevice);
|
||||||
|
|
||||||
|
WhitePixel = new Texture2D(GraphicsDevice, 1, 1);
|
||||||
|
WhitePixel.SetData(new Color[] { Color.White });
|
||||||
|
|
||||||
|
PaddleTexture = new RenderTarget2D(GraphicsDevice, 20, 80);
|
||||||
|
GraphicsDevice.SetRenderTarget(PaddleTexture);
|
||||||
|
SpriteBatch.Begin();
|
||||||
|
SpriteBatch.Draw(WhitePixel, new Rectangle(0, 0, 20, 80), Color.White);
|
||||||
|
SpriteBatch.End();
|
||||||
|
GraphicsDevice.SetRenderTarget(null);
|
||||||
|
|
||||||
|
WorldBuilder.AddOrderedRenderer(new Texture2DRenderer(SpriteBatch));
|
||||||
|
|
||||||
|
var paddle = WorldBuilder.CreateEntity();
|
||||||
|
WorldBuilder.SetComponent(paddle, new PositionComponent(new MoonTools.Structs.Position2D(5, 5)));
|
||||||
|
WorldBuilder.SetComponent(paddle, new Texture2DComponent(PaddleTexture, 0));
|
||||||
|
|
||||||
|
World = WorldBuilder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public draw() {
|
protected override void UnloadContent()
|
||||||
love.graphics.clear();
|
{
|
||||||
love.graphics.setCanvas(this.canvas);
|
base.UnloadContent();
|
||||||
love.graphics.clear();
|
}
|
||||||
this.world.draw();
|
|
||||||
love.graphics.setCanvas();
|
protected override void Update(GameTime gameTime)
|
||||||
love.graphics.setBlendMode("alpha", "premultiplied");
|
{
|
||||||
love.graphics.setColor(1, 1, 1, 1);
|
World.Update(gameTime.ElapsedGameTime.TotalSeconds);
|
||||||
love.graphics.draw(this.canvas);
|
|
||||||
|
base.Update(gameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Draw(GameTime gameTime)
|
||||||
|
{
|
||||||
|
GraphicsDevice.Clear(Color.CornflowerBlue);
|
||||||
|
|
||||||
|
SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied);
|
||||||
|
World.Draw();
|
||||||
|
SpriteBatch.End();
|
||||||
|
|
||||||
|
base.Draw(gameTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
|
@ -6,17 +6,60 @@ weight: 10
|
||||||
|
|
||||||
This one is pretty simple. We can't draw something if we don't know *where* on screen to draw it.
|
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.
|
Well, why didn't we put that in the Texture2DComponent? The reason is that position is a concept that is relevant in more situations than just drawing. For example: collision, yeah? So it really should be its own component.
|
||||||
|
|
||||||
Create a file: **game/components/position.ts**
|
As with most concepts in game programming, we find that something as simple as position is a little more nuanced than appears at first glance. Think about your computer monitor. How is it drawing things? Your screen is divided into millions of little points, and each point lights up a certain color. These are *pixels*. So... what is a fraction of a pixel? Doesn't really make sense right? It would be much easier for us to think of position as being based on integers rather than decimals.
|
||||||
|
|
||||||
```ts
|
However, if you treat positions as simple integer pixels, you will run into problems if you ever have movement speeds based on fractions (which are very useful). Fortunately, I have created a system for handling this! It's called `MoonTools.Structs`. Let's add it as a dependency.
|
||||||
import { Component } from "encompass-ecs";
|
|
||||||
|
|
||||||
export class PositionComponent extends Component {
|
{{% notice info %}}
|
||||||
public x: number;
|
There are actually techniques for *subpixel* rendering. But let's ignore that for now.
|
||||||
public y: number;
|
{{% /notice %}}
|
||||||
|
|
||||||
|
Go to your terminal program, make sure that you are in the PongFE top-level directory, and do:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git add submodule https://gitea.moonside.games/MoonsideGames/MoonTools.Structs.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, add the project as a *ProjectReference* in PongFE.Framework.csproj:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FNA\FNA.csproj"/>
|
||||||
|
<ProjectReference Include="..\encompass-cs\encompass-cs\encompass-cs.csproj" />
|
||||||
|
<ProjectReference Include="..\MoonTools.Structs\Structs\Structs.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
and PongFE.Core.csproj:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FNA\FNA.Core.csproj"/>
|
||||||
|
<ProjectReference Include="..\encompass-cs\encompass-cs\encompass-cs.csproj" />
|
||||||
|
<ProjectReference Include="..\MoonTools.Structs\Structs\Structs.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
Now we can define our PositionComponent. Create a file: **PongFE/Components/PositionComponent.cs**
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using MoonTools.Structs;
|
||||||
|
|
||||||
|
namespace PongFE.Components
|
||||||
|
{
|
||||||
|
public struct PositionComponent : IComponent
|
||||||
|
{
|
||||||
|
public Position2D Position { get; }
|
||||||
|
|
||||||
|
public PositionComponent(Position2D position)
|
||||||
|
{
|
||||||
|
Position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
That's it! Notice how short and readable these definitions are so far? I hope you're starting to notice the power of modularity here.
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
title: "Texture Component"
|
||||||
|
date: 2019-05-23T11:26:31-07:00
|
||||||
|
weight: 5
|
||||||
|
---
|
||||||
|
|
||||||
|
In a 2D game using FNA, the main way you will be drawing elements to the screen is via Texture2D.
|
||||||
|
|
||||||
|
Let's set up a Texture2DComponent. To create a new Component type, we define a struct that implements the IComponent interface.
|
||||||
|
|
||||||
|
Create a file: **PongFE/Components/Texture2DComponent.cs**
|
||||||
|
|
||||||
|
```cs
|
||||||
|
using Encompass;
|
||||||
|
using Microsoft.Xna.Framework.Graphics;
|
||||||
|
|
||||||
|
namespace PongFE.Components
|
||||||
|
{
|
||||||
|
public struct Texture2DComponent : IComponent, IDrawableComponent
|
||||||
|
{
|
||||||
|
public Texture2D Texture { get; }
|
||||||
|
public int Layer { get; }
|
||||||
|
|
||||||
|
public Texture2DComponent(Texture2D texture, int layer)
|
||||||
|
{
|
||||||
|
Texture = texture;
|
||||||
|
Layer = layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There's a lot of new information here, so let's break this down a bit.
|
||||||
|
|
||||||
|
*using* means that we are using functionality provided to us by another project. In this case, Encompass provides us with the IComponent and IDrawableComponent interfaces, and FNA provides us with the Texture2D class through the Microsoft.Xna.Framework.Graphics namespace.
|
||||||
|
|
||||||
|
*namespace* is used for organization and to prevent naming collisions. Let's say that we have a Texture2DComponent here, but another library that we include also defines something called Texture2DComponent. Now we have an ambiguity! Using namespaces avoids this problem.
|
||||||
|
|
||||||
|
{{% notice note %}}
|
||||||
|
Why is the namespace Microsoft.Xna.Framework instead of just FNA? Remember that FNA is a recreation of the original Microsoft XNA libary. So the namespace has to match for old code to remain compatible.
|
||||||
|
{{% /notice %}}
|
||||||
|
|
||||||
|
*public* means you would like Texture2DComponent to be accessible in other files and projects. Most of your classes and structs will be public, though there are cases where you might want them to be *private* or *internal*, for example a utility class that you don't want to expose externally.
|
||||||
|
|
||||||
|
What is IDrawableComponent? An IDrawableComponent is an interface that lets Encompass know that the Component includes a *Layer* property, which is used for rendering. Any time you want a component to be drawn in a specific order before or after other components, you will need to declare that your component implements IDrawableComponent.
|
||||||
|
|
||||||
|
Finally, that method is called a *constructor*. When we create an instance of Texture2DComponent, we will assign it a Texture2D that has stuff drawn on it and tell it which layer to use. We'll get to all that in a minute.
|
||||||
|
|
||||||
|
That's it for our component. We need one more bit of information before we can write our Renderer.
|
|
@ -10,10 +10,10 @@ Everyone has played, or at least heard of, Pong. Right? Right...
|
||||||
|
|
||||||
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 MECS architecture, so I think it's a good choice to try re-implementing this game 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 MECS architecture, so I think it's a good choice to try re-implementing this game 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!
|
We'll be developing this with the Encompass/FNA starter pack. Go ahead and [set that up]({{< ref "case_study_fna" >}}) if you haven't already so you can follow along. And please do follow along - you can do it!
|
||||||
|
|
||||||
{{% notice tip %}}
|
{{% notice tip %}}
|
||||||
I recommend following along with the tutorial by actually typing out the code rather than cut-and-pasting. You'll be able to follow the structure of what's happening much better. Think of it like taking notes.
|
I recommend following along with the tutorial by actually typing out the code rather than cut-and-pasting. You'll be able to follow the structure of what's happening much better. Think of it like taking notes.
|
||||||
{{% /notice %}}
|
{{% /notice %}}
|
||||||
|
|
||||||
If at any point you become confused in the tutorial, feel free to ask questions in our [Discord server](https://discord.gg/ZYE88fK) or file an issue report on our [GitHub Issues page](https://github.com/encompass-ecs/docs/issues). You can also take a look at the exact tutorial code in the [GitHub repo](https://github.com/encompass-ecs/encompass-pong-example).
|
If at any point you become confused in the tutorial, feel free to ask questions in our [Discord server](https://discord.gg/ZYE88fK) or file an issue report on our [Gitea Issues page](https://gitea.moonside.games/MoonsideGames/encompass-cs-docs/issues). You can also take a look at the exact tutorial code in the [GitHub repo](https://github.com/encompass-ecs/encompass-pong-example).
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<a href="{{ $.Site.BaseURL }}"><img src="{{ $.Site.BaseURL }}/images/logo-and-text.png" width="200"></a>
|
<a href="{{ $.Site.BaseURL }}"><img src="/images/logo-and-text.png" width="200"></a>
|
||||||
|
|
|
@ -131,3 +131,57 @@ a:hover {
|
||||||
#sidebar hr {
|
#sidebar hr {
|
||||||
border-color: var(--MENU-SECTION-HR-color);
|
border-color: var(--MENU-SECTION-HR-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overrides because the default Learn theme adds terrible heading bars to each paragraph */
|
||||||
|
|
||||||
|
div.notices p:first-child:before {
|
||||||
|
position: absolute;
|
||||||
|
top: -28px;
|
||||||
|
color: #fff;
|
||||||
|
font-family: "Font Awesome 5 Free";
|
||||||
|
font-weight: 900;
|
||||||
|
content: "\f06a";
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
div.notices p:first-child:after {
|
||||||
|
position: absolute;
|
||||||
|
top: -28px;
|
||||||
|
color: #fff;
|
||||||
|
left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.info {
|
||||||
|
border-top: 30px solid #F0B37E;
|
||||||
|
background: #FFF2DB;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.info p {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.note {
|
||||||
|
border-top: 30px solid #6AB0DE;
|
||||||
|
background: #E7F2FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.note p {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.warning {
|
||||||
|
border-top: 30px solid rgba(217, 83, 79, 0.8);
|
||||||
|
background: #FAE2E2;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.warning p {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.tip {
|
||||||
|
border-top: 30px solid rgba(92, 184, 92, 0.8);
|
||||||
|
background: #E6F9E6;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.notices.tip p {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 165 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 242 KiB |
Binary file not shown.
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 238 KiB |
|
@ -1 +1 @@
|
||||||
Subproject commit 33aa1d5079abc916b119dfe2fc51b788afb7208f
|
Subproject commit ff7aa2a40c2f122ceb502150d77b0c55f4ce7878
|
Loading…
Reference in New Issue