{-# 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! -- -- 