mischief-render-0.1.0.0: src/Mischief/Render/Tutorial/Start/Start.hs
{-# OPTIONS_GHC -Wno-unused-imports #-}
-- |
-- [Next Chapter: Your First Animation]("Mischief.Render.Tutorial.Start.Animation")
--
-- [Main Page]("Mischief.Render")
module Mischief.Render.Tutorial.Start.Start
( -- * 1.1. Starting Out
-- $intro
-- * Your First Sprite
-- $sprites
-- * Your Second Sprite
-- $second
-- * Moving Your Sprite
-- $movement
-- * Sorting Your Sprites
-- $sorting
-- * [Next Chapter: Your First Animation]("Mischief.Render.Tutorial.Start.Animation")
)
where
import Control.Monad (void)
import Mischief.Assets
import Mischief.ECS
import Mischief.Materials.Sprite (SpriteMaterial)
import Mischief.Math
import Mischief.Render
import Mischief.Render.Plugin
-- $intro
-- This chapter will walk you through rendering and moving a few sprites.
--
-- Throughout this tutorial we will be using various resources from the @assets/@ folder in the [repo](https://codeberg.org/mischief/mischief).
--
-- You can find the entire code [here](https://codeberg.org/mischief/mischief/src/branch/main/mischief-render/examples/start).
-- $sprites
-- Download @characters.png@ from the above link and place it in an @assets/@ folder at the root of your project. These
-- are sprites from a free asset pack on [Kenney](https://kenney.nl/).
--
-- Create a new Mischief app, such as this:
--
-- @
-- import "Mischief.ECS"
-- import "Mischief.Math"
-- import "Mischief.Assets"
-- import "Mischief.Render"
-- import "Mischief.Input"
-- import "Mischief.Materials.Sprite"('SpriteMaterial')
--
-- main :: 'IO' ()
-- main = do
-- app <- 'newApp'
-- 'setAssetSource' \"assets\/\" app
-- 'addPlugin' \@MainPlugin app
-- 'runApp' app
--
-- data MainPlugin
--
-- instance 'Plugin' MainPlugin where
-- 'init' = 'pure' ()
--
-- 'deps' = ['dep' \@'RenderPlugin', 'dep' \@'Mischief.Input.InputPlugin', 'dep' \@'TimePlugin']
-- @
--
-- @RenderPlugin@ is necessary for rendering. @InputPlugin@ and @TimePlugin@ we'll be using later for movement. .
--
-- Now let's write a @setup@ system that spawns a window, camera, and a sprite:
--
-- @
-- setup :: 'System' ()
-- setup = do
-- image <- 'load' \@Image \"characters.png\"
--
-- window <- 'spawn' 'Window'
-- 'spawn_' ('Camera', 'Rel' 'OutputTo' window)
--
-- 'spawn_'
-- ( 'Sprite' {image},
-- 'WithMaterial' \@'SpriteMaterial'
-- )
-- @
--
-- Don't forget to schedule it in @Startup@!
--
-- Additionally, we also need to create a system that renders the sprites:
--
-- @
-- renderSprites :: 'System' ()
-- renderSprites = 'renderMaterial' \@'SpriteMaterial' 'def'
-- @
--
-- And schedule it to run as well:
--
-- @
-- 'init' = do
-- 'systems' setup
-- & 'schedule' \@'Startup'
--
-- 'systems' renderSprites
-- & 'schedule' \@'RenderUpdate'
-- @
--
-- If you run your app now, you'll see our sprite rendered, but there's an issue! The @characters.png@ file is a spriesheet containing multiple
-- characters and animations. The whole thing is being rendered!
--
-- 
--
-- In order to limit our sprite to just a part of the texture, we can insert a @'SpriteSlice'@. Inside it, we say where the frame we want to select starts, along its size, in pixels.
--
-- @
-- 'spawn_'
-- ( 'Sprite' {image},
-- 'SpriteSlice' ('V2' 0 387) ('V2' 128 128),
-- 'WithMaterial' \@'SpriteMaterial'
-- )
-- @
--
-- @0 387@ is the position of the idle frame of the pink character in our spritesheet. Don't worry about getting the pixels yourself, we'll soon be doing this automatically through the animator.
--
-- Alright, we are now properly rendering a sprite!
--
-- 
-- $second
-- Let's give our sprite a friend! In order to position them nicely and symmetrically, we should change the transform of the first sprite to be left by some amount, and the transform of the second
-- to be right by that same amount.
--
-- This is our new spawn logic:
--
-- @
-- 'spawn_'
-- ( 'Sprite' {image},
-- 'SpriteSlice' ('V2' 0 387) ('V2' 128 128),
-- 'WithMaterial' \@'SpriteMaterial',
-- 'def' {'translation' = 'V3' (-60) 0 0}
-- )
--
-- 'spawn_'
-- ( 'Sprite' {image},
-- 'SpriteSlice' ('V2' 645 129) ('V2' 128 128),
-- 'WithMaterial' \@'SpriteMaterial',
-- 'def' {'translation' = 'V3' 60 0 0}
-- )
-- @
--
-- Alright we've successfully rendered another sprite!
--
-- 
-- $movement
-- Next, let's set up a system that moves the left sprite based on keyboard input.
--
-- First, we should make sure to mark it in some way to identify it. We can do so by creating and inserting a @Player@ component.
--
-- @
-- data Player = Player deriving ('Component')
-- @
--
-- @
--
-- @
-- 'spawn_'
-- ( 'Sprite' {image},
-- 'SpriteSlice' ('V2' 0 387) ('V2' 128 128),
-- 'WithMaterial' \@'SpriteMaterial',
-- 'def' {'translation' = 'V3' (-60) 0 0},
-- Player
-- )
-- @
--
-- You should now qualify these 3 modules. They contain utilities for processing input, working with transforms, and getting the time.
--
-- @
-- import "Mischief.Input.Keys" qualified as Keys
-- import "Mischief.Math.Transform" qualified as Transform
-- import "Mischief.ECS.Time" qualified as Time
-- @
--
-- Alright now we can finally write our movement system:
--
-- @
-- movement :: 'System' ()
-- movement = do
-- 'Just' keys <- 'res' \@Keys
-- 'Just' (player, transform) \<- 'single' ['q'|Entity, Transform \/ With Player|]
--
-- delta <- Time.'Mischief.ECS.Time.delta'
-- let speed = 150
--
-- let dirX =
-- if
-- | Keys.'Mischief.ECS.Keys.pressed' keys Keys.A -\> -1
-- | Keys.'Mischief.ECS.Keys.pressed' keys Keys.D -\> 1
-- | otherwise -\> 0
--
-- let dirY =
-- if
-- | Keys.'Mischief.ECS.Keys.pressed' keys Keys.S -\> -1
-- | Keys.'Mischief.ECS.Keys.pressed' keys Keys.W -\> 1
-- | otherwise -\> 0
--
-- let dir = 'normalize' $ 'V3' dirX dirY 0
--
-- 'insert' (Transform.'Mischief.Math.Transform.move' (dir ^* delta * speed) transform) player
-- @
--
-- It is relatively straightfoward. We get the Keys resources, along with the entity and transform of our player. The current delta time. We also set an arbitrary speed.
-- Based on the input we set the directions on the X and Y axis respectively. We put them in a V3 and normalize them. We then move the player's transform
-- by that V3 multiplied by the delta and the speed.
--
-- Don't forget to schedule this:
--
-- @
-- 'systems' movement
-- & 'schedule' \@'Update'
-- @
--
-- If you run the app now, you should be able to move the sprite!
-- $sorting
-- You may have noticed a small problem with our current app. If you overlap the two sprites,
-- one of them will always be behind the other.
--
-- 
--
-- The solution is to render the material as sorted instead. We need to provide
-- a function that, for each entity, will pick a key to sort by. The simplest value to use is
-- the sprites' y transform. That will make a sprite render under another if it is placed higher.
--
-- Let's write a function that picks the y translation of each sprite:
--
-- @
-- getY :: 'Entity' -> 'Query' 'System' 'Float'
-- getY entity = do
-- 'Transform' {'translation'} \<- ['q'|entity. Transform|]
-- 'pure' (-translation.y)
-- @
--
-- Now we can replace our sprite rendering system with this:
--
-- @
-- renderSprites :: 'System' ()
-- renderSprites = 'renderSortedMaterial' \@'SpriteMaterial' 'def' getY
-- @
--
-- Your sprites should now be getting sorted as you move them around. However, you will notice they still don't look quite right.
-- Each sprite's transform is realtive to the center of the frame by default. Meaning they get sorted by their centers, rather than
-- by the position of their feet, as you'd expect.
--
-- We can solve this easily by inserting an @Anchor@ on each sprite:
--
-- @
-- 'Anchor' $ 'V2' 0.5 0.9
-- @
--
-- Everything should be perfect now!
--
-- 