| 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 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 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 define 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 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 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 with 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, in non-quasi notation:
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
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?
There are a few different ways to get the names of entities that a given entity likes, for instance we could do:
people <-query(C@Name,R@Likes Any)for_people $ (name, likes) -> do names <-forlikes $ \l ->get(C@Name) l.targetinfo$textname <> " likes " <>textnames
We get the name of each entity, along with all their Likes relationships (using the R marker). Then, for each entity, we iterate over all their relationships and get the names of the targets.
(get is just like query but it queries the components of a specific entity).
But we'll have a way easier time getting there by just using transitive queries!
Rather than querying for the relationships themselves, Mischief allows us to query for the components of the relationship targets from within the same query:
people <-query(C@Name,R@Likes (Q(C@Name)))for_people $ (name, names) -> doinfo$textname <> " likes " <>textnames
There's no need for a second query to grab the names. The equivalent quasi-query looks like this:
people <- [q|Name, Likes -> (Name)|]
Pretty nice, huh?
Let's now put this logic in a proper system and schedule it to run:
showLikes ::System() showLikes = do people <- [q|Name, Likes -> (Name)|]for_people $ (name, names) -> doinfo$textname <> " likes " <>textnames
init_ = do Systems.addStartupaddPeople Systems.addUpdate(helloWorld, greetPeople, showLikes) Systems.addUpdate$ updateFlo`before`greetPeopleinsertRes(Greeting "Hey")
We should now see these additional likes printed to the terminal:
[INFO] "Florianne" likes ["Kimberly"] [INFO] "Kimberly" likes ["Nicholas", "Florianne"]
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 it's recommended to go check that out.
After that, the next chapters go into detail on various topics (Components, Queries, Systems, etc.), so you may choose to just read those directly, and maybe come back to the game later.