Reactor
- Package
- purescript-grid-reactors
- Repository
- Eugleo/purescript-grid-reactors
As the Pyret documentation puts it, a reactor is a value enabling the creation of time-based animations, simulations, and interactive programs. During the creation of a reactor, the user supplies a function for handling clock ticks, and functions for handling mouse and keyboard input events. The reactor calls these event handlers whenever the respective event occurs. From within the event handlers, the reactor's state — or, the world, as we call it — can be updated. After each world update, the reactor renders the new world with the supplied drawing function.
You can read more in the documentation of the type Reactor below, under the documentation for the module Reactor.Types.
The base Reactor module re-exports some useful functions, apart
from the base color pallete which is best imported by import Reactor.Graphics.Colors as Color.
In our implementation, the rendering is done using a hooks-based Halogen component.
Also, the world needs to have at least a boolean field named paused, that signalizes
to the reactor whether the internal clock should be ticking or not.
#runReactor Source
runReactor :: forall world. Reactor Aff { paused :: Boolean | world } -> Configuration -> Effect UnitStart a Reactor and render it asynchronously as a Halogen component with the given Configuration.
The reactor's world is required to have a field named paused that is used to decide
whether the reactor's clock should be running (paused: false) or not (paused: true).
Re-exports from Reactor.Action
#utilities Source
utilities :: forall world m. Action m world UtilitiesGet a record of the following:
bound :: CoordinateSystem Point -> CoordinateSystem Point, a function that bounds the given point in the grid or the canvas, meaning the point won't be beyond the bounds of its coordinate system.height :: Int,width :: Int, the dimensions of the gridcellSize :: Intthe size of one grid cell, in points. The size is set internally, and this is the only way to its value.
#togglePause Source
togglePause :: forall world m. Action m { paused :: Boolean | world } UnitToggle the paused attribute of the world from false to true and vice versa.
A shorthand for
modify_ \w -> w { paused = not w.paused }
#preventDefaultBehavior Source
preventDefaultBehavior :: forall world m. Action m { paused :: Boolean | world } DefaultBehaviorPrevent the execution of the default behavior associated with the event.
You can read more in the documentation of Reactor.Events.
Usually, this or executeDefaultBehavior is the last thing you'll call in
your onMouse and onKey handlers. This one will usually get called in
the events you handle (i.e. that have some functionality associated with them in your game).
#executeDefaultBehavior Source
executeDefaultBehavior :: forall world m. Action m { paused :: Boolean | world } DefaultBehaviorAfter handling the event, execute the default behavior as well.
You can read more in the documentation of Reactor.Events.
Usually, this or preventDefaultBehavior is the last thing you'll call in
your onMouse and onKey handlers. This one will usually get called in
the events you only let pass through (i.e. when pressing 'J' doesn't 'do anything' in your game).
Re-exports from Reactor.Events
#TickEvent Source
newtype TickEventThis event is fired on every clock-tick in the reactor.
The attribute delta is the time from the last tick, in seconds.
There's approximately 60 ticks per second, however, the number can vary depending on the browser's current available resources.
For the smoothest motion, you should calculate the traveled distance based on the
speed of the entity and the delta.
Constructors
#MouseEvent Source
data MouseEventThis event is fired whenever the user presses a button on their mouse, or moves the mouse over the rendering grid. Explanation of each of the fields:
typeis the type of the event (drag, button up, etc.)xandyare the coordinates of the event. They are relative to the rendering grid, and denote the cell where the event hapened.control,meta,shift, andaltdenote whether any modifier keys were pressed when the event happenedbuttonis an identifier of the button that has been pressed during the event, if applicable. See this entry in MDN Web Docs for more details. You pattern-match on this event in theonMousehandler.
Constructors
MouseEvent { alt :: Boolean, button :: Int, control :: Boolean, meta :: Boolean, shift :: Boolean, type :: MouseEventType, x :: Int, y :: Int }
Instances
#KeypressEvent Source
data KeypressEventThis event is fired whenever the user presses down a key on the keyboard.
The key is passed in the event as a String, with the following rules:
- Numbers, letters, and symbols all have intuitive codes (type A → get "A", type ů → get "ů").
Notably, pressing space produces an event with a literal space,
" ". - Any modifier keys that were pressed simultaneously with the main key are passed in the record
{ shift, control, alt, meta }. Ametakey is the Windows button on Windows, and the Command button on the Mac. - Common special keys have intuitive names. For example:
ArrowLeft,ArrowRight,ArrowUp,ArrowDown. Full list can be seen on MDN Web Docs. You pattern-match on this event in theonKeyhandler.
Constructors
Re-exports from Reactor.Graphics.CoordinateSystem
#wrt Source
wrt :: forall a b. a -> (a -> CoordinateSystem b) -> CoordinateSystem bWith respect to. Used to wrap things in a coordinate system in a more pleasant way than what would be possible with the constructors. For example
{ x: 1, y: 1 } `wrt` grid == RelativeToGrid { x: toNumber 1, y: toNumber 1}
#withCoords Source
withCoords :: forall a b. CoordinateSystem a -> (a -> b) -> bUnwrap the wrapped thing and pass it to a function.
#grid Source
grid :: forall a b. HMap (Int -> Number) a b => a -> CoordinateSystem bTo be used with wrt, usually { some record }wrtgrid. Automatically converts
the numbers in{ some record }fromInttoNumber`.
#canvas Source
canvas :: forall a. a -> CoordinateSystem aTo be used with wrt, usually { some record }wrt` canvas.
Re-exports from Reactor.Graphics.Drawing
Re-exports from Reactor.Page
#component Source
component :: forall world q i o m. MonadEffect m => Reactor m { paused :: Boolean | world } -> Configuration -> Component q i o mDefines the Halogen component that runs and renders a reactor. The component is based on Halogen hooks.
Usually you don't need to call this yourself; instead, you should use Reactor.runReactor that calls this internally.
Re-exports from Reactor.Types
#Reactor Source
type Reactor :: (Type -> Type) -> Type -> Typetype Reactor m world = { draw :: world -> Drawing, init :: world, onKey :: KeypressEvent -> Action m world DefaultBehavior, onMouse :: MouseEvent -> Action m world DefaultBehavior, onTick :: TickEvent -> Action m world Unit }
The reactor is a simple record. A reactor is is parametrized over the following type variables:
msignifies which monad will be used to run the event handlers, which is mostly an implementation detail. UsuallymisEffectorAff(i.e. asynchronousEffect). You can safely ignore this most of the time.worlddenotes what is the type of the internal state of the reactor. This is where the state of your game or simulation is saved.
The fields in the record are the following:
initis the initial state of the reactor's worlddrawis a function used to render the world anytime it is changedonTickis a function called on every tick of the reactor's clock. It is called around 60 times a second, provided the reactor is not paused.onKeyis an event handler for keyboard input events. It receives the pressed key and does someAction, which usually involves updating the world based on the received input.onMouseis an event handler for mouse input events (button clicks, draggging, moving). Similarly to theonKeyfunction, it receives the details of the event and does someAction, which usually involves updating the world based on the event.
For example, when ran, the following reactor would render a player at the initial position, and would allow the user to move the player by pressing arrow keys. The clock is paused, and mouse events are ignored.
import Reactor.Action
(executeDefaultBehavior, modify_, preventDefaultBehavior, utilities)
import Reactor.Events (KeypressEvent(..))
import Reactor.Graphics.Colors as Color
import Reactor.Graphics.CoordinateSystem
(CoordinateSystem, grid, moveDown, moveLeft, moveRight, moveUp, wrt)
import Reactor.Graphics.Drawing (fill, cell)
import Reactor.Types (Reactor)
type World =
{ player :: CoordinateSystem { x :: Number, y :: Number }
, paused :: Boolean
}
reactor :: forall m. Reactor m World
reactor =
{ init: { player: { x: 0, y: 0 } `wrt` grid, paused: true }
, draw: \{ player } -> fill Color.blue400 $ cell player
, onTick: \_ -> pure unit
, onKey: \(KeypressEvent key _) -> do
{ bound } <- utilities
case key of
"ArrowLeft" -> do
modify_ \w@{ player } ->
w { player = bound $ moveLeft player }
preventDefaultBehavior
"ArrowRight" -> do
modify_ \w@{ player } ->
w { player = bound $ moveRight player }
preventDefaultBehavior
"ArrowDown" -> do
modify_ \w@{ player } ->
w { player = bound $ moveDown player }
preventDefaultBehavior
"ArrowUp" -> do
modify_ \w@{ player } ->
w { player = bound $ moveUp player }
preventDefaultBehavior
_ -> executeDefaultBehavior
, onMouse: \_ -> executeDefaultBehavior
}
#Configuration Source
type Configuration = { height :: Int, title :: String, width :: Int }Configuration for the Halogen component that renders the reactor. Although a reactor is a general structure, our reactors focus on grid-based games and simulations. The component thus needs a little more info than just what is in already the reactor, namely:
title, the title of the webpage where the reactor is renderedwidth, the width of the grid, in cellsheightthe height of the grid, in cells Notably, the size of the rendered cells isn't customizable and is set to 30pts.