| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Mischief.ECS.Tutorial.Startup
Description
This module walks the user through setting up Mischief and creating a simple app.
Synopsis
Learn You an ECS for Great Mischief! - 1. Startup Guide
What do I need to know?
This book doesn't assume any knowledge of other game engines or programming paradigms, but it does expact some Haskell knowledge.
While it is possible to read this book and get a pretty good idea of what Mischief is and how it works, you'll have a much better time if you have at least a very basic understanding of Haskell syntax.
Setup
In order to use Mischief, you'll first need to install GHC and cabal. You can follow this quick-start guide in order to do that.
After you have a new project set up, you need to install mischief-ecs:
cabal install mischief-ecs
We recommend using GHC2024 as the language standard (set in your .cabal file).
Language Extensions
We generally recommend using the following language extensions in a Mischief project:
DeriveAnyClass DuplicateRecordFields NoFieldSelectors DerivingStrategies OverloadedRecordDot OverloadedStrings QuasiQuotes RequiredTypeArguments TypeFamilyDependencies
QuasiQuotes and OverloadedStrings are especially important because some Mischief features are not available without them (namely quasi-queries and logging).
The rest of the extensions are highly optional.
You can paste these extensions in the default-extensions field of your .cabal file.
The App
A Mischief program usually starts with an app and a plugin.
import Mischief.ECS.Prelude main ::IO() main = do app <-newAppMyPluginrunAppapp data MyPlugin = MyPlugin deriving (Eq,Plugin)
If you copy this code into your project and run it using cabal run, your app will start! Although we haven't told it to do anything yet.
The App is a wrapper around our World, which is the structure containing all data stored by the ECS. It allows us to add
initializition instructions and to plug additional logic into our game through Plugins.
The ECS
Mischief's ECS logic is designed to be very approachable and simple to write.
Components are just types deriving the Component typeclass.
data Position = Position {x :: Float, y :: Float} deriving (Component)
Systems are functions in the System monad.
printPositions ::System() printPositions = doinfo.text=<<query(C@Position)
Entities are ids used to represent and manipulate data.
data Entity = Entity Int
Your First System
Paste the following function into your module:
helloWorld ::System() helloWorld =info"Hello World!"
This will be our first system. The only remaining step is to schedule it to run!
import Mischief.ECS.Systems qualified as Systems
instancePluginMyPlugin whereinit:: MyPlugin ->System()init_ = do Systems.addUpdatehelloWorld
The Systems.add function adds the system to your App's Update schedule, making it run once per frame. If you run your app again,
you will see "Hello World!" printed to your terminal many, many times.
As you may have noticed, the init we give to the Plugin is, in itself, a system! There's nothing differentiating
the logic you write here from the logic ran at any point in your app's runtime. init is just a convenient way of
adding some initialization that happens before anything else, but we'll get into that later.
Your First Component
Let's do a little more than greeting the whole world, let's greet some individual people!
In ECS, you would generally model people as entities with a set of components that define them. Let's start with a simple Person component:
data Person = Person deriving (Component)
So how can we give people names? In a more traditional design you could just add a name :: String field to Person. But the ECS makes you think of it differently!
A Name is just a piece of data that can be attached to anything. A dog could also have a name. So why not just make a Name component?
data Name = NameStringderiving (Component)
No need to do it though, since this exact Name is already defined internally by Mischief, you can just use it directly.
Now that we can represent people with names, let's make a system that spawns some:
addPeople :: System ()
addPeople = do
spawn (Person, Name "Kimberly")
spawn (Person, Name "Nicholas")
spawn (Person, Name "Florian")
You can register it to run on the app's startup like this:
init_ = do Systems.addStartupaddPeople Systems.addUpdatehelloWorld
Your First Query
If you run your app, the people will be spawned but we aren't doing anything to them yet! Let's make a system that greets them:
greetPeople ::System() greetPeople = do people <-query(C@Name,C@Person)for_people $ (name, _) ->info$ "Hello " <>textname
The above query will grab the Name and Person of every entity. Then it iterates over them in order to greet them.
The Person component however, is only queried to ensure we are querying the right entities. We don't care about its value at all! So we can instead write it
as a filter to limit the types of entities selected by the query and save us the trouble of carrying an extra variable around.
greetPeople ::System() greetPeople = do people <-query'(C@Name) (With(C@Person))for_people $ name ->info$ "Hello " <>textname
Do note the use of query' here instad of query. The former is a variant of the same function but which also expects a filter.
Mischief has two equivalent ways of writing queries. The normal way that you've seen above, and the quasi way:
people <- [q|Name / With Person|]
Query-queries are macros meant to simplify writing queries. They'll become especially helpful once we start dealing with relationships and transitive queries.
Now we can schedule this system to also run:
init_ = do Systems.addStartupaddPeople Systems.addUpdate(helloWorld, greetPeople)
Running our app will result in the following output:
[INFO] Hello World! [INFO] Hello Kimberly [INFO] Hello Nicholas [INFO] Hello Florian
Note that "Hello World" might show above or beneath the other people, since systems in the same schedule can run in any order unless they are explicitly ordered.
Your First Mutation
If we want to change the name of some people, we can apply a mutation to a value obtained from the query:
updateFlo ::SystemupdateFlo = do people <- [q|Name / With Person|]for_people $ name -> dowhen(name == Name "Florian") $setname (Name "Florianne")
Although.. that feels awfully imperative doesn't it? We can also write the same system as:
updateFlo = do florians <- [q|Name / With Person, Check (== Name "Florian")|]for_florians $`set`Name "Florianne"
The above query can also be written like this:
florians <-query'(C@Name) (With(C@Person), 'Check (== Name "Florian"))
Let's add the new system to a schedule:
init_ = do Systems.addStartupaddPeople Systems.addUpdate(helloWorld, greetPeople) Systems.addUpdate$ updateFlo`before`greetPeople
Note that we have explicitly ordered updateFlo to happen before greetPeople. We want to only greet Flo after their name has changed!
Your First Resource
Resources are a great way to store global information that can be easily written to and read in any system.
Let's say we want to have a custom greeting that we can change at runtime. We could store that in a resource:
data Greeting = GreetingStringderiving (Component)
Yes, resources are just normal components! Any component can be stored and retrieved as a resource by using insertRes and res.
Let's insert a greeting from our init system:
init_ = do Systems.addStartupaddPeople Systems.addUpdate(helloWorld, greetPeople) Systems.addUpdate$ updateFlo`before`greetPeopleinsertRes(Greeting "Hey")
And let's modify greetPeople so that it uses the current greeting from the resource:
greetPeople ::System() greetPeople = doJust(Greeting greeting) <-res@Greeting people <- [q|Name / With Person]for_people $ name ->info$textgreeting <> " " <>textname
You should now see this when running the app:
[INFO] Hello World! [INFO] Hey Kimberly [INFO] Hey Nicholas [INFO] Hey Florianne