chapters 2 and 3
parent
283b456100
commit
28a4f04bba
|
@ -2,3 +2,6 @@
|
|||
path = public
|
||||
url = git@github.com:encompass-ecs/encompass-ecs.github.io.git
|
||||
branch = master
|
||||
[submodule "themes/hugo-notice"]
|
||||
path = themes/hugo-notice
|
||||
url = https://github.com/martignoni/hugo-notice.git
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
baseURL = "/"
|
||||
languageCode = "en-US"
|
||||
title = "Encompass Docs"
|
||||
theme = "hugo-theme-learn"
|
||||
theme = ["hugo-notice", "hugo-theme-learn"]
|
||||
pygmentsUseClasses = true
|
||||
pygmentsCodefences = true
|
||||
|
||||
[outputs]
|
||||
home = [ "HTML", "RSS", "JSON" ]
|
||||
|
@ -13,4 +15,3 @@ home = [ "HTML", "RSS", "JSON" ]
|
|||
showVisitedLinks = false
|
||||
disableBreadcrumb = false
|
||||
disableNextPrev = false
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
+++
|
||||
title = "Concepts"
|
||||
date = 2019-05-22T12:49:24-07:00
|
||||
weight = 15
|
||||
chapter = true
|
||||
pre = "<b>3. </b>"
|
||||
+++
|
||||
|
||||
### Chapter 3
|
||||
|
||||
# Concepts
|
||||
|
||||
It's time for some non-specifics.
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
title: "Component"
|
||||
date: 2019-05-22T12:51:29-07:00
|
||||
weight: 5
|
||||
---
|
||||
|
||||
A Component is a collection of related data.
|
||||
|
||||
To define a Component, extend the Component class.
|
||||
|
||||
```ts
|
||||
import { Component } from "encompass-ecs";
|
||||
|
||||
class PositionComponent extends Component {
|
||||
public x: number;
|
||||
public y: number;
|
||||
}
|
||||
```
|
||||
|
||||
Components are created in context with an Entity.
|
||||
|
||||
```ts
|
||||
const entity = World.create_entity();
|
||||
const position = entity.add_component(PositionComponent);
|
||||
position.x = 3;
|
||||
position.y = -4;
|
||||
```
|
||||
|
||||
Components cannot exist apart from an Entity and are automagically destroyed when they are removed or their Entity is destroyed.
|
||||
|
||||
{{% notice warning %}}
|
||||
Components should **never** reference other Components. This breaks the principle of loose coupling. You **will** regret it if you do this.
|
||||
{{% /notice %}}
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
title: "Engine"
|
||||
date: 2019-05-22T13:01:28-07:00
|
||||
weight: 15
|
||||
---
|
||||
|
||||
An Engine is the Encompass notion of an ECS System. Much like the engine on a train, your Engines make the simulation move along.
|
||||
|
||||
{{% notice notice %}}
|
||||
I never liked the term System. It is typically used to mean structures in game design and I found this confusing when discussing code implementation vs design.
|
||||
{{% /notice %}}
|
||||
|
||||
Engines are responsible for reading the game state, reading messages, emitting messages, and creating or mutating Entities and Components.
|
||||
|
||||
An Engine which Reads a particular message is guaranteed to run *after* all Engines which Emit that particular message.
|
||||
|
||||
To define an Engine, extend the Engine class.
|
||||
|
||||
Here is an example Engine:
|
||||
|
||||
```ts
|
||||
import { Engine, Mutates, Reads } from "encompass-ecs";
|
||||
import { LogoUIComponent } from "../../components/ui/logo";
|
||||
import { ShowUIMessage } from "../../messages/show_ui";
|
||||
|
||||
@Reads(ShowUIMessage)
|
||||
@Mutates(LogoUIComponent)
|
||||
export class LogoDisplayEngine extends Engine {
|
||||
public update() {
|
||||
const logo_ui_component = this.read_component_mutable(LogoUIComponent);
|
||||
if (logo_ui_component && this.some(ShowUIMessage)) {
|
||||
logo_ui_component.image.isVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If a LogoUIComponent exists, and a ShowUIMessage is received, it will make the logo image on the LogoUIComponent visible. Simple!
|
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
title: "Entity"
|
||||
date: 2019-05-22T12:55:22-07:00
|
||||
weight: 10
|
||||
---
|
||||
|
||||
An Entity is a structure composed of a unique ID and a collection of Components.
|
||||
|
||||
Entities do not have any implicit properties or behaviors. They are granted these by their collection of Components.
|
||||
|
||||
There is no limit to the amount of Components an Entity may have, and Entities can have any number of Components of a particular type.
|
||||
|
||||
Entities are active by default and can be deactivated. They can also be destroyed, permanently removing them and their components from the World.
|
||||
|
||||
Entities are created either by the WorldBuilder or by Engines. (We'll get into these soon.)
|
||||
|
||||
{{% notice warning %}}
|
||||
You should **never** add methods or properties to an Entity. This is what Components are for.
|
||||
{{% /notice %}}
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: "Message"
|
||||
date: 2019-05-22T13:10:43-07:00
|
||||
weight: 7
|
||||
---
|
||||
|
||||
Similar to Components, Messages are collections of data.
|
||||
|
||||
Messages are used to transmit data between Engines so they can manipulate the game state accordingly.
|
||||
|
||||
To define a message, extend the Message class.
|
||||
|
||||
```ts
|
||||
import { Message } from "encompass-ecs";
|
||||
|
||||
class MotionMessage extends Message {
|
||||
public x: number;
|
||||
public y: number;
|
||||
}
|
||||
```
|
||||
|
||||
Messages are temporary and destroyed at the end of the frame.
|
||||
|
||||
{{% notice notice %}}
|
||||
Ok fine, since you asked, Messages actually live in an object pool so that they aren't garbage-collected at runtime. But you as the game developer don't have to worry about that.
|
||||
{{% /notice %}}
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
title: "World"
|
||||
date: 2019-05-22T12:51:08-07:00
|
||||
weight: 100
|
||||
---
|
||||
|
||||
World is the pie crust that contains all the delicious Encompass ingredients together.
|
||||
|
||||
The World's *update* function drives the simulation and should be controlled from your engine's update loop.
|
||||
|
||||
The World's *draw* function tells the Renderers to draw the scene.
|
||||
|
||||
In LÖVE, the starter project game loop looks like this:
|
||||
|
||||
```ts
|
||||
export class Game {
|
||||
private world: World;
|
||||
private canvas: Canvas;
|
||||
|
||||
...
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
But you can call these methods wherever you see fit.
|
||||
|
||||
{{% notice tip %}}
|
||||
Certain Encompass projects actually have multiple separate Worlds to manage certain behaviors. This is perfectly valid and can be a great way to structure your project, but be warned that it is difficult to share information between Worlds by design.
|
||||
{{% /notice %}}
|
||||
|
||||
That's it! Now that we have these high-level concepts down, let's build an actual, for-real game.
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
title: "World Builder"
|
||||
date: 2019-05-22T13:40:25-07:00
|
||||
weight: 35
|
||||
---
|
||||
|
||||
WorldBuilder is used to construct a World from Engines, Renderers, and an initial state of Entities, Components, and Messages.
|
||||
|
||||
The WorldBuilder enforces certain rules about Engine structure. It is forbidden to have messages create cycles between Engines, and no Component may be mutated by more than one Engine.
|
||||
|
||||
The WorldBuilder uses Engines and their Message read/emit information to determine a valid ordering of the Engines, which is given to the World.
|
||||
|
||||
Here is an example usage:
|
||||
|
||||
```ts
|
||||
class Game {
|
||||
private World world;
|
||||
|
||||
...
|
||||
|
||||
public load() {
|
||||
import { WorldBuilder } from "encompass-ecs";
|
||||
import { CanvasComponent } from "./components/canvas";
|
||||
import { PositionComponent } from "./components/position";
|
||||
import { VelocityComponent } from "./components/velocity";
|
||||
import { MotionEngine } from "./engines/motion";
|
||||
import { CanvasRenderer } from "./renderers/canvas";
|
||||
|
||||
const world_builder = new WorldBuilder();
|
||||
|
||||
world_builder.add_engine(MotionEngine);
|
||||
world_builder.add_renderer(CanvasRenderer);
|
||||
|
||||
const entity = world_builder.create_entity();
|
||||
|
||||
const position_component = entity.add_component(PositionComponent);
|
||||
position_component.x = 0;
|
||||
position_component.y = 0;
|
||||
|
||||
const velocity_component = entity.add_component(VelocityComponent);
|
||||
velocity_component.x = 20;
|
||||
velocity_component.y = 0;
|
||||
|
||||
const sprite_component = entity.add_component(SpriteComponent);
|
||||
canvas_component.canvas = love.graphics.newImage("assets/sprites/ball.png");
|
||||
|
||||
this.world = world_builder.build();
|
||||
}
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Now our game will initialize with a ball that moves horizontally across the screen!
|
||||
|
||||
{{% notice tip %}}
|
||||
Make sure that you remember to add Engines to the WorldBuilder when you define them. Otherwise nothing will happen, which can be very embarrassing.
|
||||
{{% /notice %}}
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
title: "Case Study: LÖVE"
|
||||
date: 2019-05-22T10:38:01-07:00
|
||||
weight: 10
|
||||
---
|
||||
|
||||
First, install [LÖVE](https://love2d.org).
|
||||
|
||||
Make sure you can run it from the terminal by running:
|
||||
|
||||
```sh
|
||||
> love
|
||||
```
|
||||
|
||||
You should see the LÖVE window pop up with the NO GAME screen. If you don't see this, check your terminal environment. On Windows you probably need to manually add the path where you installed LÖVE to your Path Environment Variable and then restart the machine. Thanks Windows!
|
||||
|
||||
Encompass-TS uses [Node.js](https://nodejs.org/) for its build process and [npm](https://www.npmjs.com/) to manage dependencies.
|
||||
Install Node, which automatically installs npm.
|
||||
|
||||
Make sure it is properly installed by running:
|
||||
|
||||
```sh
|
||||
> npm
|
||||
```
|
||||
|
||||
You should see npm print out a bunch of help information. Now we're almost ready to begin.
|
||||
|
||||
Download the [Encompass/LÖVE starter project](https://github.com/encompass-ecs/encompass-love2d-starter). Place its contents in a folder and rename the folder to the name of your project. Change information in the `package.json` file where appropriate.
|
||||
|
||||
Now we are ready. Enter the project folder in your terminal and do:
|
||||
|
||||
```sh
|
||||
> npm install
|
||||
```
|
||||
|
||||
Encompass-TS will install everything it needs to compile your project to Lua.
|
||||
|
||||
The starter project contains some scripts to automate the build process.
|
||||
|
||||
To run your game, do:
|
||||
|
||||
```sh
|
||||
> npm run love
|
||||
```
|
||||
|
||||
Or, on Windows:
|
||||
|
||||
```sh
|
||||
> npm run lovec
|
||||
```
|
||||
|
||||
so that you get proper debug console output.
|
||||
|
||||
If you just want to build the game without running it, do:
|
||||
|
||||
```sh
|
||||
> npm run build
|
||||
```
|
||||
|
||||
That's everything you need to start making a game with Encompass and LÖVE! Wasn't that easy?
|
|
@ -0,0 +1,32 @@
|
|||
---
|
||||
title: "Choosing An Engine"
|
||||
date: 2019-05-22T09:53:32-07:00
|
||||
weight: 5
|
||||
---
|
||||
|
||||
Encompass is not a game engine.
|
||||
|
||||
It does not provide any tools for drawing graphics to the screen, playing sounds, handling inputs, or anything of the sort.
|
||||
|
||||
Encompass is purely a tool for managing the code that handles the *simulation* aspects of your game.
|
||||
|
||||
That means it needs to run on top of an engine.
|
||||
|
||||
**Which engine should I use?**
|
||||
|
||||
Ultimately, this is a question that you have to answer for your project. Is there an engine you're already comfortable with? Which platforms are you targeting? Linux? PS4? Android? Are there any features you would really like to have, like a built-in physics simulator? These are questions that could help you choose an engine.
|
||||
|
||||
Encompass-TS can hook into any engine that supports JavaScript or Lua scripting.
|
||||
Encompass-CS can hook into any engine that supports C# scripting.
|
||||
|
||||
So you have a lot of choices!
|
||||
|
||||
Here are some engines that I have used:
|
||||
|
||||
[LÖVE](https://love2d.org/) is a wonderful little framework for 2D games. It's cross-platform and very lightweight, but with a lot of powerful features, including a complete physics simulator. It uses Lua scripting, so you would want Encompass-TS.
|
||||
|
||||
[MonoGame](http://www.monogame.net/) is a cross-platform 2D/3D framework that thousands of games have used. You can use it to ship games on basically any platform that exists and it is extremely well-supported. It uses C# scripting.
|
||||
|
||||
[BabylonJS](https://www.babylonjs.com/) uses the power of WebGL to run 3D games in the browser. It has a powerful graphics pipeline and you can make stuff that has some wow factor. It runs on JS.
|
||||
|
||||
Encompass gives you the power to develop using many different engines, so feel free to experiment and find one you like! And if you switch to an engine that uses the same scripting language, it's actually very easy to switch engines, because the simulation layer is mostly self-contained.
|
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
title: "Project Structure"
|
||||
date: 2019-05-22T12:19:55-07:00
|
||||
weight: 15
|
||||
---
|
||||
|
||||
Structuring your project is a crucial component of keeping your Encompass development sane. Let's review how an Encompass project is typically structured.
|
||||
|
||||
If we look at the Encompass/LÖVE starter project, it looks like this:
|
||||
|
||||
![the directory structure of the starter project](/images/structure.png)
|
||||
|
||||
**main.ts** is the entry point of the game for LÖVE. You can change top-level configuration things here, like setting the window size or whether the mouse is visible. You should review the [LÖVE Documentation](https://love2d.org/wiki/Main_Page) for more clarification.
|
||||
|
||||
**conf.ts** defines certain information about the game. You can read more about LÖVE Config Files [here](https://love2d.org/wiki/Config_Files).
|
||||
|
||||
**game/game.ts** contains a class that defines three methods that are called by LOVE: load, update, and draw. They do what it says on the tin. You will set up your WorldBuilder in the load method. (We'll talk about that in a bit.)
|
||||
|
||||
The rest of it is pretty straightforward. Put your music and sprites and such in the **assets** folder. Define your components in the **components** folder, your engines in the **engines** folder, your messages in the **messages** folder, and your renderers in the **renderers** folder. (Again, we'll start getting into exactly how to define Encompass these in a minute.)
|
||||
|
||||
Finally, a quick note about **helpers**. I like to use classes with static methods for common behaviors that will be useful for many different engines, for example a `Color` class with a `hsv_to_rgb` conversion function. Be careful not to abuse helpers. If your helpers need to be instantiated, that is usually a sign that the behavior belongs in an engine.
|
|
@ -4,7 +4,7 @@ date: 2019-05-21T15:56:13-07:00
|
|||
weight: 30
|
||||
---
|
||||
|
||||
Hyper ECS is a new architecture pattern that attempts to address some common issues with standard ECS.
|
||||
Hyper ECS is a new architecture pattern that attempts to address some common issues with standard ECS. This is the architecture that Encompass implements.
|
||||
|
||||
The core of the architecture is the introduction of a new construct to ECS: the **Message**.
|
||||
|
||||
|
@ -24,7 +24,7 @@ Now we have our MotionSystem. The MotionSystem declares that it Mutates the Tran
|
|||
|
||||
You might be wondering: how does the game know which order these systems need to be in? Well...
|
||||
|
||||
**Encompass figures it out for you.**
|
||||
**Hyper ECS figures it out for you.**
|
||||
|
||||
That's right! With the power of graph theory, we can construct an order for our Systems so that any System which Emits a certain Message runs before any System that Reads the same Message. This means, when you write behavior for your game, you *never* have to specify the order in which your Systems run. You simply write code, and the Systems run in a valid order, every time, without surprising you.
|
||||
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/* Background */ .chroma { color: #f8f8f2; background-color: #272822 }
|
||||
/* Error */ .chroma .err { color: #960050; background-color: #1e0010 }
|
||||
/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||
/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; }
|
||||
/* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc }
|
||||
/* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||
/* Keyword */ .chroma .k { color: #66d9ef }
|
||||
/* KeywordConstant */ .chroma .kc { color: #66d9ef }
|
||||
/* KeywordDeclaration */ .chroma .kd { color: #66d9ef }
|
||||
/* KeywordNamespace */ .chroma .kn { color: #f92672 }
|
||||
/* KeywordPseudo */ .chroma .kp { color: #66d9ef }
|
||||
/* KeywordReserved */ .chroma .kr { color: #66d9ef }
|
||||
/* KeywordType */ .chroma .kt { color: #66d9ef }
|
||||
/* NameAttribute */ .chroma .na { color: #a6e22e }
|
||||
/* NameClass */ .chroma .nc { color: #a6e22e }
|
||||
/* NameConstant */ .chroma .no { color: #66d9ef }
|
||||
/* NameDecorator */ .chroma .nd { color: #a6e22e }
|
||||
/* NameException */ .chroma .ne { color: #a6e22e }
|
||||
/* NameFunction */ .chroma .nf { color: #a6e22e }
|
||||
/* NameOther */ .chroma .nx { color: #a6e22e }
|
||||
/* NameTag */ .chroma .nt { color: #f92672 }
|
||||
/* Literal */ .chroma .l { color: #ae81ff }
|
||||
/* LiteralDate */ .chroma .ld { color: #e6db74 }
|
||||
/* LiteralString */ .chroma .s { color: #e6db74 }
|
||||
/* LiteralStringAffix */ .chroma .sa { color: #e6db74 }
|
||||
/* LiteralStringBacktick */ .chroma .sb { color: #e6db74 }
|
||||
/* LiteralStringChar */ .chroma .sc { color: #e6db74 }
|
||||
/* LiteralStringDelimiter */ .chroma .dl { color: #e6db74 }
|
||||
/* LiteralStringDoc */ .chroma .sd { color: #e6db74 }
|
||||
/* LiteralStringDouble */ .chroma .s2 { color: #e6db74 }
|
||||
/* LiteralStringEscape */ .chroma .se { color: #ae81ff }
|
||||
/* LiteralStringHeredoc */ .chroma .sh { color: #e6db74 }
|
||||
/* LiteralStringInterpol */ .chroma .si { color: #e6db74 }
|
||||
/* LiteralStringOther */ .chroma .sx { color: #e6db74 }
|
||||
/* LiteralStringRegex */ .chroma .sr { color: #e6db74 }
|
||||
/* LiteralStringSingle */ .chroma .s1 { color: #e6db74 }
|
||||
/* LiteralStringSymbol */ .chroma .ss { color: #e6db74 }
|
||||
/* LiteralNumber */ .chroma .m { color: #ae81ff }
|
||||
/* LiteralNumberBin */ .chroma .mb { color: #ae81ff }
|
||||
/* LiteralNumberFloat */ .chroma .mf { color: #ae81ff }
|
||||
/* LiteralNumberHex */ .chroma .mh { color: #ae81ff }
|
||||
/* LiteralNumberInteger */ .chroma .mi { color: #ae81ff }
|
||||
/* LiteralNumberIntegerLong */ .chroma .il { color: #ae81ff }
|
||||
/* LiteralNumberOct */ .chroma .mo { color: #ae81ff }
|
||||
/* Operator */ .chroma .o { color: #f92672 }
|
||||
/* OperatorWord */ .chroma .ow { color: #f92672 }
|
||||
/* Comment */ .chroma .c { color: #75715e }
|
||||
/* CommentHashbang */ .chroma .ch { color: #75715e }
|
||||
/* CommentMultiline */ .chroma .cm { color: #75715e }
|
||||
/* CommentSingle */ .chroma .c1 { color: #75715e }
|
||||
/* CommentSpecial */ .chroma .cs { color: #75715e }
|
||||
/* CommentPreproc */ .chroma .cp { color: #75715e }
|
||||
/* CommentPreprocFile */ .chroma .cpf { color: #75715e }
|
||||
/* GenericDeleted */ .chroma .gd { color: #f92672 }
|
||||
/* GenericEmph */ .chroma .ge { font-style: italic }
|
||||
/* GenericInserted */ .chroma .gi { color: #a6e22e }
|
||||
/* GenericStrong */ .chroma .gs { font-weight: bold }
|
||||
/* GenericSubheading */ .chroma .gu { color: #75715e }
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1 @@
|
|||
Subproject commit 43a8c75474e010ea393356bdfe3614c86485b12e
|
|
@ -18,6 +18,7 @@
|
|||
<link href="{{"css/atom-one-dark-reasonable.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
|
||||
<link href="{{"css/theme.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
|
||||
<link href="{{"css/hugo-theme.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
|
||||
<link href="{{"css/syntax.css" | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
|
||||
{{with .Site.Params.themeVariant}}
|
||||
<link href="{{(printf "css/theme-%s.css" .) | relURL}}{{ if $assetBusting }}?{{ now.Unix }}{{ end }}" rel="stylesheet">
|
||||
{{end}}
|
||||
|
@ -71,7 +72,7 @@
|
|||
{{if $showBreadcrumb}}
|
||||
{{ template "breadcrumb" dict "page" . "value" .Title }}
|
||||
{{ else }}
|
||||
{{ .Title }}
|
||||
{{ .Title }}
|
||||
{{ end }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -101,7 +102,7 @@
|
|||
{{$parent := .page.Parent }}
|
||||
{{ if $parent }}
|
||||
{{ $value := (printf "<a href='%s'>%s</a> > %s" $parent.URL $parent.Title .value) }}
|
||||
{{ template "breadcrumb" dict "page" $parent "value" $value }}
|
||||
{{ template "breadcrumb" dict "page" $parent "value" $value }}
|
||||
{{else}}
|
||||
{{.value|safeHTML}}
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in New Issue