Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[developer] Intermediate guide: Best Practices for EE developers #99

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
218 changes: 2 additions & 216 deletions docs/developer/typescript/01_gettingStarted/00_intro.md
Original file line number Diff line number Diff line change
@@ -1,216 +1,2 @@
# Getting Started with Typescript
<!--
NOTE: This page should contain:
- Hero Project: Showcase for Ethereal Engine's development tools and workflows.
- Guide: Teaches a new user how to program the Hero Project and be comfortable with EE project development.
- Segue: Lead the user into the Developer Manual
-->
_This guide will teach you how to get started programming with Ethereal Engine using Typescript._
_Visit the [Typescript: Introduction](/docs/manual/developer/typescript/intro) page for more details._
<!-- TODO: Add intro text as a mdx partial, instead of linking to the other page -->

## Overview

We're going to look at 'Pong', a multiplayer game that we've built in Ethereal Engine using Typescript. It is an example of best practices for developers.

## Installation and Running Pong

1) Ethereal Engine scans for projects mounted in the /packages/projects/projects sub-folder of Ethereal Engine. From scratch we can install Ethereal Engine itself and also register a sub project using the following:

```
gh repo clone EtherealEngine/EtherealEngine
cd EtherealEngine
cd packages/projects/projects
gh repo clone EtherealEngine/ee-tutorial-pong
cd ../../../
```

2) A fresh install of Ethereal Engine can be run like so:

```
npm install
npm run dev
```

3) Once Ethereal Engine itself is running, from the web admin panel of Ethereal Engine create a 'location' for the project. See https://localhost:3000/admin . Map the project to the name 'pong'.

4) Run the project on the web by visiting it with the URL you created. See https://localhost:3000/location/pong

## Understanding Pong

### A 10000 foot overview

Pong has several files which are roughly represent parts of the experience:

- PaddleSystem -> a paddle 'system' for managing the player paddles specifically.
- PlateComponent -> each pong game has one or more plates that represent player positions in a game. In our case we have 4 player games.
- PongComponent -> each pong game in a larger pong world has a concept of a game starting or stopping; this is managed here.
- PongGameSystem -> a game 'system' for managing each game instance; starting and stopping play and dealing with players.
- PongPhysicsSystem -> a ball spawner and scoring 'system' for the pong game overall
- worldinjection -> bootstraps the game

A 'Pong' world can have several separate pong tables at once. Each pong table has four pong plates and can handle four players at once.

### World Injection

Ethereal Engine projects typically have a worldinjection.ts file to bootstrap a project. In Pong this file registers and starts three 'systems', and does little else:

```
import './PaddleSystem'
import './PongGameSystem'
import './PongPhysicsSystem'
export default async function worldInjection() {}
```

### Entities, Components and Systems

Ethereal Engine uses an ECS (Entity Component System). Entities in a game are entirely defined by their Components or 'capabilities', and systems driven the state of the entire application over time by running every frame over those components. One benefit of an ECS is that it allows a highly granular 'vocabulary' for designers to express game ideas with.

The ECS we use currently BitECS. Another good ECS is Flecs:

- https://github.com/NateTheGreatt/bitECS
- https://news.ycombinator.com/item?id=35434374

In Pong there are three systems imported via worldInjection above. Each is responsible for a different part of the experience:

1) PaddleSystem
2) PongPhysicsSystem
3) PongGameSystem

We will look at the PaddleSystem.ts first.

### PaddleSystem: Introduction to State and Reactivity

Ethereal Engine uses the React pattern of allowing state observers to 'react' to state changes. This is done for a couple of different reasons. Philosophically it separates the 'what' from the 'how', and technically it helps decouple game components from each other, allowing developers to scale work horizontally, reducing dependencies.

The React website has several good discussions on reactivity as a whole:

- https://react.dev/learn/reacting-to-input-with-state

In Pong each active player has two paddles (one for the left hand and one for the right hand), and paddles are spawned or destroyed as players come and go. In this case the 'what' is that there are some set of paddles. And the 'how' (which we will get to later) is that the paddles happen to be 3d objects with collision hulls. But at this level of scope we can separate our concerns and we don't have to think about how the paddles are manifested.

In PaddleSystem.ts we see a good example of this reactive state pattern. The approach we've taken here is to track the enumeration of active paddles like so:

```
export const PaddleState = defineState({
name: 'ee.pong.PaddleState',
initial: {} as Record<
EntityUUID,
{
owner: UserID
handedness: 'left' | 'right'
gameEntityUUID: EntityUUID
}
>
...
```

The defineState() method registers a collection of Record objects. A Record is a schema in a third party runtime schema definition language that Ethereal Engine uses.

### PaddleSystem: Introduction to Event Sourced State

Ethereal Engine uses an event sourced state paradigm. Sourcing state and responding to that state is asynchronous but a single 'effect' or outcome results, rather than having to propagate potentially thousands of successive state changes.

A good discussion of Event Sourcing can be found here:

https://domaincentric.net/blog/event-sourcing-snapshotting

In an Event Sourced system, the current state of an aggregate is usually reconstituted from the full history of events. It means that before handling a command we need to do a full read of a single fine-grained stream and transport the events over the network. This allows late joiners to synchronize with the overall game state.

In PaddleSystem we define a set of actions explicitly like so:

```
export class PaddleActions {
static spawnPaddle = defineAction({
...WorldNetworkAction.spawnObject.actionShape,
prefab: 'ee.pong.paddle',
gameEntityUUID: matchesEntityUUID,
handedness: matches.literals('left', 'right'),
owner: matchesUserId,
$topic: NetworkTopics.world
})
}
```

And we then allow the registration of 'receptors' on state objects to catch dispatched events over the network, and in this case we're entirely focused on updating the state records above:

```
...
receptors: [
[
PaddleActions.spawnPaddle,
(state, action: typeof PaddleActions.spawnPaddle.matches._TYPE) => {
state[action.entityUUID].merge({
owner: action.owner,
handedness: action.handedness,
gameEntityUUID: action.gameEntityUUID
})
}
],
[
WorldNetworkAction.destroyObject,
(state, action: typeof WorldNetworkAction.destroyObject.matches._TYPE) => {
state[action.entityUUID].set(none)
}
]
]
})
```

The WorldNetworkAction.destroyObject is an observer we've injected here to catch here to make sure that we update our state tables appropriately. Although we have custom state on the object creation, we don't have any custom state on paddle destruction.

### PaddleState: Introduction to Components

With the state management out of the way, now we're left with the details of making sure our visual representations reflect our state.

PaddleReactor defines a React component that has a useEffect() to observe state changes on a given PaddleState entry. When the PaddleState changes it sets up an entity to reflect that owner. Inside the useEffect() we see several typical 3d and game related components being setup:

- UUIDComponent
- TransformComponent
- VisibleComponent
- DistanceFromCameraComponent
- FrustrumCullComponent
- NameComponent
- PrimitiveGeometryComponent
- ColliderComponent
- GrabbableComponent

Most of these components are self descriptive, and this typically reflects the core set of components you'll see in many Ethereal Engine 3d entities that represent objects in a game.

The GrabbableComponent is notable in that it's a good example of where components are more than just 'state'; they can be used to form an expressive 'vocabulary' of high level intentions. In this case we want the paddle to stay attached to the owner avatar at a specified attachment point. If we didn't have this concept we would have to fire rigid body physics target position events every frame to keep the paddle synchronized with the player.

### PaddleState: Introduction to Reactors

Both PaddleReactor and Reactor in PaddleSystem demonstrate reactivity to state. The reactor is updated whenever state changes, and the game entities that exist are a reflection of that larger state.

### PaddleState: System

Tying everything together in PaddleSystem is the PaddleSystem itself. It registers and runs an execute() handler every frame and it also registers the reactor:

```
export const PaddleSystem = defineSystem({
uuid: 'pong.paddle-system',
execute,
reactor,
insert: { after: PhysicsSystem }
})
```

### PaddleState: Overall Flow

The general flow is like so:

1) The execute handler catches and handles PaddleActions using ```receiveActions(PaddleState)```

2) The PaddleActions respond to network events and applies them to the PaddleState.

3) The reactor reacts to any state changes on PaddleState.

## PlateComponent


### PongComponent, PongGameSystem and PongPhysicsSystem


### Summary

# Topic 1
Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.
2 changes: 0 additions & 2 deletions docs/developer/typescript/02_topic1/00_intro.md

This file was deleted.

2 changes: 0 additions & 2 deletions docs/developer/typescript/02_topic1/01_file.md

This file was deleted.

2 changes: 0 additions & 2 deletions docs/developer/typescript/02_topic1/02_file2.md

This file was deleted.

8 changes: 0 additions & 8 deletions docs/developer/typescript/02_topic1/_category_.json

This file was deleted.

55 changes: 55 additions & 0 deletions docs/developer/typescript/50_bestPractices/00_intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
sidebar_label: Typescript
---
# Typescript: Best Practices Guide
<!--
NOTE: This page should contain:
- Hero Project: Showcase for Ethereal Engine's development tools and workflows.
- Guide: Teaches a new user how to program the Hero Project and be comfortable with EE project development.
- Segue: Lead the user into the Developer Manual
-->

:::important
This guide teaches some of the Ethereal Engine recommended best practices for developers.
As such, the level of knowledge required will be higher than if it was just an introductory guide.

_Visit the [Developer Manual](/docs/manual/developer/intro) page for in-depth information about programming with Ethereal Engine._
:::

This guide will teach you how to create **Pong**, a multiplayer game built with Ethereal Engine using Typescript.

## Installation and Running the project
### 1. Install Pong
<!-- TODO: This should be an MDX partial that sends the user to the developer quick-start guide for running projects with a local environment. -->
Ethereal Engine scans for projects in its `/packages/projects/projects` sub-folder.
We can install Ethereal Engine from scratch and also register a sub project using the following commands:
```bash
git clone https://github.com/EtherealEngine/etherealengine
cd etherealengine
cd packages/projects/projects
git clone https://github.com/EtherealEngine/ee-tutorial-pong
cd ../../../
```

### 2. Run the engine
A fresh install of Ethereal Engine can be run with:
```bash
npm install
npm run dev
```

### 3. Configure the Location
Go to the Admin Panel page, create a `Location` for the project and change the project's name to `pong`.
:::note[Admin Panel]
https://localhost:3000/admin
:::

:::important
Ethereal Engine must be running for this step and the rest of this guide to work.
:::

### 4. Run Pong
Run the project on the web by visiting it with the URL you just created.
:::note[Pong]
https://localhost:3000/location/pong
:::
Loading