Slide 1

Slide 1 text

Manuel M T Chakravarty & Gabriele Keller Applicative & UNSW Sydney Haskell SpriteKit A Purely Functional API for a Stateful Animation System & Physics Engine mchakravarty TacticalGrace justtesting.org haskellformac.com

Slide 2

Slide 2 text

The Dilemma

Slide 3

Slide 3 text

The Dilemma

Slide 4

Slide 4 text

The Dilemma SKShapeNode *shipOverlayShape = [[SKShapeNode alloc] init]; shipOverlayShape.path = boundingPath; shipOverlayShape.strokeColor = [SKColor clearColor]; shipOverlayShape.fillColor = [SKColor colorWithRed:0.0 green:1.0 blue:0.0 alpha:0.5]; [ship addChild:shipOverlayShape];

Slide 5

Slide 5 text

The Dilemma SKShapeNode *shipOverlayShape = [[SKShapeNode alloc] init]; shipOverlayShape.path = boundingPath; shipOverlayShape.strokeColor = [SKColor clearColor]; shipOverlayShape.fillColor = [SKColor colorWithRed:0.0 green:1.0 blue:0.0 alpha:0.5]; [ship addChild:shipOverlayShape];

Slide 6

Slide 6 text

The Dilemma SKShapeNode *shipOverlayShape = [[SKShapeNode alloc] init]; shipOverlayShape.path = boundingPath; shipOverlayShape.strokeColor = [SKColor clearColor]; shipOverlayShape.fillColor = [SKColor colorWithRed:0.0 green:1.0 blue:0.0 alpha:0.5]; [ship addChild:shipOverlayShape]; data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] }

Slide 7

Slide 7 text

The Compromise Purely Functional API data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] }

Slide 8

Slide 8 text

The Compromise Purely Functional API data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented

Slide 9

Slide 9 text

The Compromise Purely Functional API represent scenes with algebraic data types data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented

Slide 10

Slide 10 text

The Compromise Purely Functional API translate transformations into state changes represent scenes with algebraic data types data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented

Slide 11

Slide 11 text

Purely Functional API data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented

Slide 12

Slide 12 text

Purely Functional API data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented Step ❶ — state of the art

Slide 13

Slide 13 text

Purely Functional API data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented Step ❶ — state of the art Step ❷ — purification

Slide 14

Slide 14 text

Purely Functional API data GameState = GameState { gsSize :: (Double, Double) , time :: Millisecond , level :: Int , lifes :: Int , towers :: [Tower] , creeps :: [Creep] , shots :: [Shot] } functional imperative object-oriented Step ❶ — state of the art Step ❷ — purification Step ❸ — efficiency

Slide 15

Slide 15 text

SpriteKit Physics, fields & particles Animation & lighting Joints & constraints

Slide 16

Slide 16 text

SpriteKit Physics, fields & particles Animation & lighting Joints & constraints Lazy Lambda Simple side scroller

Slide 17

Slide 17 text

Step ❶ The State of the Art

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

No content

Slide 22

Slide 22 text

0 pipes moving nodes pipe pair l ground physics score contact l l

Slide 23

Slide 23 text

— node without visuals 0 pipes moving nodes pipe pair l ground physics score contact l l

Slide 24

Slide 24 text

— node without visuals — attached actions 0 pipes moving nodes pipe pair l ground physics score contact l l

Slide 25

Slide 25 text

— node without visuals — attached actions — attached physics body 0 pipes moving nodes pipe pair l ground physics score contact l l

Slide 26

Slide 26 text

— node without visuals — attached actions — attached physics body 0 pipes moving nodes pipe pair l ground physics score contact l l

Slide 27

Slide 27 text

0 pipes moving nodes pipe pair l ground physics score contact l l SKScene SKLabelNode SKNode SKNode SKSpriteNode SKNode SKNode SKNode SKSpriteNode SKSpriteNode SKSpriteNode SKSpriteNode

Slide 28

Slide 28 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode OOism #1: Subclassing

Slide 29

Slide 29 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ OOism #1: Subclassing

Slide 30

Slide 30 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ OOism #1: Subclassing class SKNode: … { var frame: CGRect func calculatedFrame() -> CGRect var position: CGPoint var zRotation: CGFloat var xScale: CGFloat ⋮ }

Slide 31

Slide 31 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ OOism #1: Subclassing this one is odd class SKNode: … { var frame: CGRect func calculatedFrame() -> CGRect var position: CGPoint var zRotation: CGFloat var xScale: CGFloat ⋮ }

Slide 32

Slide 32 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ NSResponder LambdaScene

Slide 33

Slide 33 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ NSResponder LambdaScene class NSResponder: NSObject { func mouseDown(with: NSEvent) func mouseUp(with: NSEvent) func keyDown(with: NSEvent) func keyUp(with: NSEvent) ⋮ }

Slide 34

Slide 34 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ NSResponder LambdaScene class NSResponder: NSObject { func mouseDown(with: NSEvent) func mouseUp(with: NSEvent) func keyDown(with: NSEvent) func keyUp(with: NSEvent) ⋮ } user-defined subclass to override event handlers

Slide 35

Slide 35 text

OOism #2: Mutable Properties

Slide 36

Slide 36 text

OOism #2: Mutable Properties Lambda tilts (rotation) depending on vertical velocity

Slide 37

Slide 37 text

OOism #2: Mutable Properties class SKNode: … { var frame: CGRect var position: CGPoint var zRotation: CGFloat var xScale: CGFloat ⋮ } Lambda tilts (rotation) depending on vertical velocity mutation

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

0 pipes moving nodes pipe pair l ground physics score contact l l

Slide 40

Slide 40 text

0 pipes moving nodes pipe pair l ground physics score contact l l class SKScene: SKEffectNode { var backgroundColor: NSColor func update(_ currentTime: TimeInterval) ⋮ }

Slide 41

Slide 41 text

0 pipes moving nodes pipe pair l ground physics score contact l l class SKScene: SKEffectNode { var backgroundColor: NSColor func update(_ currentTime: TimeInterval) ⋮ } called once per frame

Slide 42

Slide 42 text

OOism #3: In-place Graph Edits

Slide 43

Slide 43 text

OOism #3: In-place Graph Edits Pipe pairs appear and disappear at regular intervals

Slide 44

Slide 44 text

OOism #3: In-place Graph Edits Pipe pairs appear and disappear at regular intervals class SKNode: … { var children: [SKNode] { get } func addChild(_ node: SKNode) ⋮ } mutation

Slide 45

Slide 45 text

0 pipes moving nodes ground physics l l pipe pair score contact

Slide 46

Slide 46 text

0 pipes moving nodes ground physics l l pipe pair score contact pipe pair score contact

Slide 47

Slide 47 text

0 pipes moving nodes ground physics l l pipe pair score contact pipe pair score contact pipe pair score contact

Slide 48

Slide 48 text

0 pipes moving nodes ground physics l l pipe pair score contact pipe pair score contact

Slide 49

Slide 49 text

The Three Issues

Slide 50

Slide 50 text

The Three Issues Subclassing Inherited properties & event handlers

Slide 51

Slide 51 text

The Three Issues Subclassing Inherited properties & event handlers Mutable properties Inplace updates by callbacks

Slide 52

Slide 52 text

The Three Issues Subclassing Inherited properties & event handlers Mutable properties Inplace updates by callbacks Inplace graph edits Add, delete & rearrange nodes

Slide 53

Slide 53 text

Step ❷ Pure Functions & Datatypes

Slide 54

Slide 54 text

Subclassing Inherited properties & event handlers

Slide 55

Slide 55 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ

Slide 56

Slide 56 text

SKLabelNode SKScene SKNode SKSpriteNode SKEffectNode SKShapeNode SKLightNode ộ

Slide 57

Slide 57 text

SKNode SKLabelNode SKSpriteNode SKShapeNode

Slide 58

Slide 58 text

data Node u = Node { nodeName :: Maybe String , nodePosition :: Point , nodeZRotation :: GFloat , nodeUserData :: u ⋮ } | Label { nodeName :: Maybe String , nodePosition :: Point , nodeZRotation :: GFloat , nodeUserData :: u ⋮ , labelText :: String ⋮ } | Sprite { … } | Shape { … } ⋮

Slide 59

Slide 59 text

data Node u = Node { nodeName :: Maybe String , nodePosition :: Point , nodeZRotation :: GFloat , nodeUserData :: u ⋮ } | Label { nodeName :: Maybe String , nodePosition :: Point , nodeZRotation :: GFloat , nodeUserData :: u ⋮ , labelText :: String ⋮ } | Sprite { … } | Shape { … } ⋮ shared fields of ”superclass” node included in each variant

Slide 60

Slide 60 text

data Node u = Node { nodeName :: Maybe String , nodePosition :: Point , nodeZRotation :: GFloat , nodeUserData :: u ⋮ } | Label { nodeName :: Maybe String , nodePosition :: Point , nodeZRotation :: GFloat , nodeUserData :: u ⋮ , labelText :: String ⋮ } | Sprite { … } | Shape { … } ⋮ shared fields of ”superclass” node included in each variant each variant (except Node) has specific fields

Slide 61

Slide 61 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneData :: sceneData , sceneBackgroundColor :: Color ⋮ }

Slide 62

Slide 62 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneData :: sceneData , sceneBackgroundColor :: Color ⋮ } children of a Scene are all Nodes

Slide 63

Slide 63 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneData :: sceneData , sceneBackgroundColor :: Color ⋮ } children of a Scene are all Nodes the Scene and the Nodes may contain extra user-defined data

Slide 64

Slide 64 text

No content

Slide 65

Slide 65 text

Mutable properties Inplace updates by callbacks

Slide 66

Slide 66 text

data Node u = Node { … } | Sprite { nodeName :: Maybe String , nodeZRotation :: GFloat ⋮ } ⋮ data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] ⋮ }

Slide 67

Slide 67 text

data Node u = Node { … } | Sprite { nodeName :: Maybe String , nodeZRotation :: GFloat ⋮ } ⋮ data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] ⋮ } immutable, but must change to tilt the bird

Slide 68

Slide 68 text

data Node u = Node { … } | Sprite { nodeName :: Maybe String , nodeZRotation :: GFloat ⋮ } ⋮ data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneUpdate :: Maybe (SceneUpdate sceneData nodeData) ⋮ } immutable, but must change to tilt the bird

Slide 69

Slide 69 text

data Node u = Node { … } | Sprite { nodeName :: Maybe String , nodeZRotation :: GFloat ⋮ } ⋮ data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneUpdate :: Maybe (SceneUpdate sceneData nodeData) ⋮ } immutable, but must change to tilt the bird callback called once per frame

Slide 70

Slide 70 text

data Node u = Node { … } | Sprite { nodeName :: Maybe String , nodeZRotation :: GFloat ⋮ } ⋮ data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneUpdate :: Maybe (SceneUpdate sceneData nodeData) ⋮ } immutable, but must change to tilt the bird type SceneUpdate sd nd = Scene sd nd -> TimeInterval -> Scene sd nd callback called once per frame pure scene transformer

Slide 71

Slide 71 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneUpdate :: Maybe (SceneUpdate sceneData nodeData) ⋮ } type SceneUpdate sd nd = Scene sd nd -> TimeInterval -> Scene sd nd

Slide 72

Slide 72 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneUpdate :: Maybe (SceneUpdate sceneData nodeData) , sceneHandleEvent :: Maybe (EventHandler sceneData) ⋮ } type SceneUpdate sd nd = Scene sd nd -> TimeInterval -> Scene sd nd type EventHandler sceneData = Event -> sceneData -> Maybe sceneData

Slide 73

Slide 73 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneUpdate :: Maybe (SceneUpdate sceneData nodeData) , sceneHandleEvent :: Maybe (EventHandler sceneData) ⋮ } type SceneUpdate sd nd = Scene sd nd -> TimeInterval -> Scene sd nd type EventHandler sceneData = Event -> sceneData -> Maybe sceneData transforms game state in response to input events

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

Inplace graph edits Add, delete & rearrange nodes

Slide 76

Slide 76 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneData :: sceneData ⋮ } data Node u = Node { … , nodeChildren :: [Node u] ⋮ } | Label { … , nodeChildren :: [Node u] ⋮ } ⋮

Slide 77

Slide 77 text

data Scene sceneData nodeData = Scene { sceneName :: Maybe String , sceneChildren :: [Node nodeData] , sceneData :: sceneData ⋮ } data Node u = Node { … , nodeChildren :: [Node u] ⋮ } | Label { … , nodeChildren :: [Node u] ⋮ } ⋮ these fields describe the scene tree

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

Step ❸ Hidden State & Efficient Marshalling

Slide 80

Slide 80 text

0 pip movin groun l l pip score

Slide 81

Slide 81 text

0 pip movin groun l l pip score ObjC -> Haskell

Slide 82

Slide 82 text

0 pip movin groun l l pip score spawnPipePair ObjC -> Haskell

Slide 83

Slide 83 text

0 pip movin groun l l pip score 0 pi movin groun l l pip score pip score spawnPipePair new subtree ObjC -> Haskell Haskell -> ObjC

Slide 84

Slide 84 text

0 pip movin groun l l pip score 0 pi movin groun l l pip score pip score spawnPipePair new subtree old subtree ObjC -> Haskell Haskell -> ObjC

Slide 85

Slide 85 text

Eager Marshalling Is Infeasible

Slide 86

Slide 86 text

Eager Marshalling Is Infeasible Hidden State Nodes have private state that would get lost

Slide 87

Slide 87 text

Eager Marshalling Is Infeasible Hidden State Nodes have private state that would get lost Efficiency Marshalling a whole tree to change a few properties

Slide 88

Slide 88 text

data Scene sc nd = Scene { … } — Haskell record type SceneUpdate sc nd = Scene sc nd -> TimeInterval -> Scene sc nd once per frame 0 moving nodes ground physics l l l

Slide 89

Slide 89 text

data Scene sc nd = Scene { … } — Haskell record type SceneUpdate sc nd = Scene sc nd -> TimeInterval -> Scene sc nd once per frame Scene sc nd lazy marshalling 0 moving nodes ground physics l l l

Slide 90

Slide 90 text

data Scene sc nd = Scene { … } — Haskell record type SceneUpdate sc nd = Scene sc nd -> TimeInterval -> Scene sc nd once per frame Scene sc nd lazy marshalling SceneUpdate sc nd Scene sc nd 0 moving nodes ground physics l l l

Slide 91

Slide 91 text

data Scene sc nd = Scene { … } — Haskell record type SceneUpdate sc nd = Scene sc nd -> TimeInterval -> Scene sc nd once per frame Scene sc nd lazy marshalling SceneUpdate sc nd Scene sc nd compute diff 0 moving nodes ground physics l l l

Slide 92

Slide 92 text

data Scene sc nd = Scene { … } — Haskell record type SceneUpdate sc nd = Scene sc nd -> TimeInterval -> Scene sc nd once per frame Scene sc nd lazy marshalling SceneUpdate sc nd Scene sc nd compute diff apply changes 0 moving nodes ground physics l l l

Slide 93

Slide 93 text

Lazy Marshalling 0 moving nodes ground physics l l l

Slide 94

Slide 94 text

Lazy Marshalling 0 moving nodes ground physics l l l lazy marshalling Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ }

Slide 95

Slide 95 text

Lazy Marshalling 0 moving nodes ground physics l l l lazy marshalling Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } thunks (suspended computations)

Slide 96

Slide 96 text

Lazy Marshalling 0 moving nodes ground physics l l l lazy marshalling Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } thunks (suspended computations)

Slide 97

Slide 97 text

Lazy Marshalling marshalSKScene :: SKScene -> Scene sceneData nodeData marshalSKScene skScene = Scene { sceneName = unsafePerformIO $(objc … ) , sceneChildren = unsafePerformIO $ do { nodes <- $(objc … ) ; unsafeInterleaveNSArrayTolistOfNode nodes } ⋮ }

Slide 98

Slide 98 text

Lazy Marshalling marshalSKScene :: SKScene -> Scene sceneData nodeData marshalSKScene skScene = Scene { sceneName = unsafePerformIO $(objc … ) , sceneChildren = unsafePerformIO $ do { nodes <- $(objc … ) ; unsafeInterleaveNSArrayTolistOfNode nodes } ⋮ } produces a thunk (unevaluated expression)

Slide 99

Slide 99 text

Lazy Marshalling marshalSKScene :: SKScene -> Scene sceneData nodeData marshalSKScene skScene = Scene { sceneName = unsafePerformIO $(objc … ) , sceneChildren = unsafePerformIO $ do { nodes <- $(objc … ) ; unsafeInterleaveNSArrayTolistOfNode nodes } ⋮ } produces a thunk (unevaluated expression) inline Objective-C

Slide 100

Slide 100 text

0 moving nodes ground physics l l l Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } Compute Diff & Update

Slide 101

Slide 101 text

0 moving nodes ground physics l l l Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } Compute Diff & Update Scene { sceneName = ”New Name” , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } SceneUpdate sc nd

Slide 102

Slide 102 text

0 moving nodes ground physics l l l Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } Compute Diff & Update Scene { sceneName = ”New Name” , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } SceneUpdate sc nd compute diff

Slide 103

Slide 103 text

0 moving nodes ground physics l l l Scene { sceneName = , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } Compute Diff & Update Scene { sceneName = ”New Name” , sceneChildren = , sceneData = , sceneBackgroundColor = ⋮ } SceneUpdate sc nd compute diff

Slide 104

Slide 104 text

Compute Diff & Update marshalScene :: Scene sceneData nodeData — original scene -> Scene sceneData nodeData — updated scene -> IO SKScene marshalScene originalScene Scene{..} = do { … ; case reallyUnsafePtrEquality# originalName sceneName of 1# -> return () _ -> $(objc …) ; updateChildren skNode originalChildren sceneChildren ⋮ }

Slide 105

Slide 105 text

Compute Diff & Update marshalScene :: Scene sceneData nodeData — original scene -> Scene sceneData nodeData — updated scene -> IO SKScene marshalScene originalScene Scene{..} = do { … ; case reallyUnsafePtrEquality# originalName sceneName of 1# -> return () _ -> $(objc …) ; updateChildren skNode originalChildren sceneChildren ⋮ }

Slide 106

Slide 106 text

Object Caching & Identity

Slide 107

Slide 107 text

Object Caching & Identity data Node u = Node { nodeName :: Maybe String , nodePosition :: Point ⋮ , nodeForeign :: Maybe SKNode } | Label { nodeName :: Maybe String , nodePosition :: Point ⋮ , nodeForeign :: Maybe SKNode , labelText :: String ⋮ } ⋮ if available, basis for marshalling this node to ObjC land

Slide 108

Slide 108 text

Haskell SpriteKit is open source! https://github.com/mchakravarty/HaskellSpriteKit mchakravarty TacticalGrace justtesting.org haskellformac.com >< state types Want to see the gory details?

Slide 109

Slide 109 text

Thank you!

Slide 110

Slide 110 text

Image Attribution https://pixabay.com/photo-51675/ https://pixabay.com/photo-509871/ https://pixabay.com/photo-1294844/