| Safe Haskell | None |
|---|---|
| Language | GHC2024 |
Mischief.ECS.Tutorial.Startup
Contents
- Learn You an ECS for Great Mischief! - 1. Startup Guide
- What do I need to know?
- Setup
- Text
- The ECS
- The App
- Your First System
- Your First Component
- Your First Query
- Your First Mutation
- Your First Resource
- Your First Relationship
- Your First Transitive Query
- Explanation: From
- What's Next?
- Next Chapter: Coding a Dungeon Game
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
This chapter will guide you through setting up a working Mischief app and performing some simple operations on data.
What do I need to know?
This book doesn't assume any knowledge of other game engines or programming paradigms, but it does expect 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, just add mischief-ecs under build-depends in you .cabal file.
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 MultiWayIf 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.
Text
Mischief uses Text instead of String where possible, including in its logging system.
The i macro from string-interpolate is re-exported by Mischief and will be often used for logging in this tutorial.
Sometimes functions from Data.Text are used as well (such as T.show), so it's recommended to add text as a dependency to your project and to qualify it when you want to use it:
import Data.Text qualified as T
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.
updatePositions ::System() updatePosition = [q|Position, Velocity|] &qinsert((Position p, Velocity v) -> (Position p + v)) &query_
Entities are opaque ids used to represent and manipulate data.
data Entity = Entity Int
The App
A Mischief program usually starts with creating an App and adding a plugin to it. So let's do that!
import Mischief.ECS.Prelude main ::IO() main = do app <-newAppaddPlugin@MyPlugin apprunAppapp data MyPlugin instancePluginMyPlugin
Your First System
Copy the following function into your file:
helloWorld ::System() helloWorld =info"Hello World!"
This will be our first system. It just logs a message saying "Hello World!". The only remaining step is to schedule it to run!
instancePluginMyPlugin whereinit::System()init= dosystemshelloWorld &schedule@Update
The systems function will grab the system for us, and schedule @Update will add it to the 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.
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 write this one though, since this exact Name is already defined internally by Mischief and exported by the Prelude.
Now that we can represent people with names, let's make a system that spawns some:
addPeople ::System() addPeople = do kim <-spawn(Person, Name "Kimberly") nick <-spawn(Person, Name "Nicholas") flo <-spawn(Person, Name "Florian")pure()
You can register it to run on the app's Startup schedule, making it run only once, at the start:
instancePluginMyPlugin where init = dosystemshelloWorld &schedule@UpdatesystemsaddPeople &schedule@Startup
Your First Query
If you run your app, the people will be spawned but we aren't doing anything with them yet! Let's make a system that greets them:
greetPeople ::System() greetPeople = do people <-query[q|Name, Person|]for_people $ \(name, _) -> doinfo[i|Hello #{name}!|]
The above query function will grab the Name and Person of every entity. We then iterate 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[q|Name / With Person|]for_people $ \name ->info[i|Hello #{name}!|]
With Person is a filter, telling our query builder to only select entities with the Person component. We use / to separate
the data that we're querying from the filters.
Additionally, Mischief lets you write and process queries by piping dedicated functions into each other. For instance, our earlier function is equivalent to:
greetPeople ::System() greetPeople = do [q|Name / With Person|] &qinfo(\name -> [i|Hello #{name}!|]) &query_
The quasi-query ([q|..|]) produces our query, we then use qinfo to display a message to the terminal for each element in the query,
and finally we use query_ to run all the commands and discard the results (the normal query returns the results).
Now we can schedule this system to also run:
instancePluginMyPlugin where init = dosystems(helloWorld, greePeople) &schedule@UpdatesystemsaddPeople &schedule@Startup
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 others. That's because 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 ::System() updateFlo = do people <-query[q|Entity, Name / With Person|]for_people $ \(entity, name) -> dowhen(name == Name "Florian") $insert(Name "Florianne") entity
We are querying the name of each entity (Name), along with its actual id (Entity). We then iterate over all the names, and once we see "Florian", we
re-insert the component, changing its value to "Florianne". Re-insertion is the main way to mutate data in Mischief.
Although.. that feels awfully imperative doesn't it? Let's rewrite the same system, this time using piping:
updateFlo ::System() updateFlo = do [q|Name / With Person|] &qfilter(== Name "Florian") &qinsert(\_ -> Name "Florianne") &query_
This time we apply a filter over our Query, leaving only those entities with their name set to "Florian". We then use qinsert to
map the old name to the new one and insert it on the entity.
Let's add the new system to a schedule:
init= dosystems(helloWorld, greetPeople) &schedule@UpdatesystemsaddPeople &schedule@StartupsystemsupdateFlo &beforegreetPeople &schedule@Update
Note that we have explicitly ordered updateFlo to happen before greetPeople. We want to only greet Flo after their name has changed! Running the app
should now show "Hello Florianne!" instead of "Florian".
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!
We'll also give it a Show instance to make printing it easier:
instanceShowGreeting whereshow(Greeting a) = a
Let's insert a greeting from our init system:
init= dosystems(helloWorld, greetPeople) &schedule@UpdatesystemsaddPeople &schedule@StartupsystemsupdateFlo &beforegreetPeople &schedule@UpdateinsertRes(Greeting "Hey")
insertRes inserts the corresponding resource into the World.
And let's modify greetPeople so that it uses the current greeting from the resource:
greetPeople ::System() greetPeople = doJustgreeting <-res@Greeting [q|Name / With Person|] &qinfo(\name -> [i|#{greeting} #{name}!|]) &query_
You should now see this when running the app:
[INFO] Hello World! [INFO] Hey Kimberly! [INFO] Hey Nicholas! [INFO] Hey Florianne!
Your First Relationship
Relationships in Mischief are pairs made up of a Component and an Entity. Let's implement a simple relationship between our entities that says which like which.
We'll start by defining a component:
data Likes = Likes deriving (Component)
Let's now modify our spawning system to also insert relationships between our three entities. We can insert a relationship using the Rel keyword.
addPeople ::System() addPeople = do kim <-spawn(Person, Name "Kimberly") nick <-spawn(Person, Name "Nicholas") flo <-spawn(Person, Name "Florian")insert(RelLikes kim) floinsert(RelLikes nick,RelLikes flo) kim
We've now made flo like kim, and we've made kim like both nick and flo!.
Your First Transitive Query
We now have relationships but we aren't doing much with them. What about having a system that displays the name of each entity, along with the name of all entities they like?
We can make use of a mechanism called a transitive query, which look like this:
showLikes ::System() showLikes = do [q|Name, Likes -> (Name)|] &qinfo(\(name, likes) -> [i|#{name} likes #{likes}|]) &query_
Pretty cool, huh?
Now let's schedule our new system to run:
init= dosystems(helloWorld, greetPeople, showLikes) &schedule@UpdatesystemsaddPeople &schedule@StartupsystemsupdateFlo &before(greetPeople, showLikes) &schedule@UpdateinsertRes(Greeting "Hey")
We should now see these additional lines printed to the terminal:
[INFO] Florianne likes [From (42v1, Kimberly)] [INFO] Kimberly likes [From (44v1, Nicholas),From (45v1, Florianne)]
Explanation: From
You may have noticed earlier, when we query for -> (Name) and then print the names, we don't get the actual names, but instead
something that looks like From (42v1, Kimberly).
That's because, when doing transitive queries, the components come wrapped in this:
data From c = From {comp :: c, entity :: Entity}
They have a different origin entity than the other components in the query, and this is our main way of keeping track of that.
To get rid of the wrapper we can just change the query to this:
showLikes ::System() showLikes = do [q|Name, Likes -> (Name)|] &qinfo(\(name, likes) -> [i|#{name} likes #{map(.comp) likes}|]) &query_
You can find more information on this in the Components Chapter.
What's Next?
What you learn next is up to you.
The next chapter will have you working on a little dungeon game in the terminal and introduce you to more notions. If you prefer to learn by example I would recommended checking that out.
Then there's a chapter which goes over different situations and problems you may encounter and describes various solutions to them.
After that, there are chapters giving you a technical overview for various parts of the ECS (Components, Queries, Systems, and so on).