Archived entries from file /Users/alexkehayias/Dropbox/Private/Clojure/chocolatier/todo.org
- [X] Update to version of clojurescipt and Austin plugin
- [ ] Sound system
- [ ] Tile collision
- [ ] Map to Screen coords
- [ ] Text rendering component
- [-] Optimization
- [ ] Batch render sprites Move sprites to a new display object group and render all at once
- [ ] Reducers? Investigate translating the game state transformations of systems and components as one giant reduction
- [X] core.async to run systems async when marked as async-able
- This turned out to be a very slow idea since just about everything so far is cpu bound
- [ ] Game loop macro
- [X] Helper macros/functions for declaring the game state
- [X] Initialize state
- [X] Initialize components
- [X] Initialize entities
- Subscribe events
- Init component state
- [X] Update render-sprite to check for a moveable state before attempting to set a position
- This means a sprite does not require a moveable component either
- [X] Update mk-sprite! to optionally take a frame this will force the rendering
- Show red text with the hit amount taken
- Fade out after a short duration
- Should follow the entity
- Move sprites to a separate place instead of component state?
- This would eliminate the coupling of multiple components that need to alter the sprite from having to know about each other
- Would not need to be animateable to have a position
- Should there be a sprite component, position component, animation component?
- Sprite component would do all the altering at the same time on the sprite
- Sprite would only need to read the position component, animation component to know what to do
- [X] Create a text component
- Initial state takes some text
- Must be created with a position component
- Don’t want a text-movement system
- [X] Browser repl
- [X] Centralized logging
- [X] Scenes (collection of systems)
- [X] Systems (collection of component functions)
- [X] Entities (UIDs with collection of component IDs)
- [X] State (nested hashmap representing scene state)
- [X] Messaging (Event bus for sending and receiving messages between systems)
- [X] Movement
- [X] Circle collision detection
- [X] Rendering
- [X] User input
- [X] Tile mapping
- [X] Debug layer (collision circles)
- [X] Replay game state (go back to old game state on the fly)
- [X] Animations (sprites)
- What if we just wrap all the higher order fns with defs?
- What if at the time of wiring we def everything or stick them the game state in a deftype?
- engine.core/mk-game-state
- Maybe ces/mk-component should def the functions then return the fully qualified names in the state hashmap
- We already have a static number keys for the game state and should not allow anything else
- scenes
- game
- systems
- components
- entities
- This didn’t end up helping since I found that these issues only show up when dynamically loading things from the repl
- Topics
- Profiling
- Functional game engine design
- Performance optimization of ClojureScript
- Embedded repl as a game mechanic
- As per this blog post http://swannodette.github.io/2015/03/16/optimizing-clojurescript-function-invocation/ we can get better call site optimization
- Need extern file to work with advanced Google Closure optimization i.e https://github.com/cljsjs/packages/blob/master/d3/resources/cljsjs/d3/common/d3.ext.js
- Alternatively, refer to external libs by string name only (let [yay ((goog.object.get js/window “yayQuery”))] ((goog.object.get yay “sayHello”) ((goog.object.get yay “getMessage”))))
- [ ] Extern file for pixijs
- [ ] Extern file for howlerjs
- [ ] Update project.clj under cljsbuild profile “min” build ;; Statically analyze function arrities :static true ;; Flatten namespace conventions into ;; names instead of nested objects :pseudo-names true
- Benchmarks http://0fps.net/2015/01/23/collision-detection-part-3-benchmarks/
- Good r-tree lib https://github.com/mourner/rbush
- According to profiler it’s really slow
- Seems like the root cause is get-multi-component-state function that only gets realized inside of update-spatial-index
- When you double the number of entities the framerate drops nearly in half
- AI system/component showcases this the best, removing it increases the framerate a ton
- How can we reduce the number of changes to the global state?
- Size of the state hashmap is not the problem Adding initial state of (reduce into {} (map hash-map (range 10000) (range 10000))) has no slow down even with 10000 additional keys
- Event messages the biggest issue?
- Every time a system runs it can generate up to n number of state changes where n is the number of events emitted
- Make the event queue a mutable object
- Was super slow, see mutable-queue branch
- Every time an event is emitted, mutate the queue instead of rewriting the state hashmap
- Make the queue a hashmap instead of a hashmap and an array
- Each message has it’s own unique ID
- To get all messages at a selector, iterate through all keys at that point
- Only once per game loop will we need to return a new game state for the sake of events (when the queues are cleared)
- Flatten the keys for events and
- Batch all the events in one shot at the end of the system call\
- Too slow Profiler shows bottleneck in emit-events and get-events
- Currently implemented as a nested hashmap of sequences of event hashmaps
- [X] Remove nesting?
- Want a way to select what we need in one shot withouth walking a nested data structure
- Should reduce time for getting events and emitting events if there is no nesting
- Overload the names of the event to prevent nesting?
- Do we need to give up hierarchical event subscriptions? Are they useful?
- Removed nesting and that improves by a few frames
- [ ] Remove events in favor of directly querying component state
- Would couple components implicitely
- Is there a way to explicitely make the dependencies between entities and components states?
- Instead of inbox, maybe replace with a read only queryable component state?
- If you have direct access to objects then they can be mutated by something other than the component that owns the state
- Components shouldn’t have access to game meta state only component state
- Would couple components implicitely
- [-] Reduce the usage of events
- Movement/collision/animation is the challenging part to decouple
:player1
controllable – moveable – collision – position – animation
:enemy1 ai – moveable – collision – position – animation
- [ ] Make a new component called position which is based on the movement and collision component states
- [ ] Player1 movement component should query the input state and set the desired movement
- [ ] Position component
- [ ] For enemies, position component to set the offsets
- [ ] Animateable queries position and action component state
- [X] Use state machines instead of requiring a message to be present
Example: Only send an event if something has changed instead of requiring a message every frame
- [X] Update controllable/react-to-input to only emit an event if the state has changed
- [X] Update moveable to only emit when movement has changed
- Movement/collision/animation is the challenging part to decouple
:player1
controllable – moveable – collision – position – animation
- It would be hard to had additional functionality to component functions if we have to worry about breaking call signature changes
- [X] Change args to all component functions to be a single hashmap
- [ ] Rename :args-fn to something more appropriate
- Error is “bad value context for arguments value”
- bevry/taskgroup#12 (comment)
- https://code.google.com/p/v8/issues/detail?id=3037
- Fixed by making it a two arrity function
- Use simple-benchmark which is built in which shows the running time for a function over n iterations
- [X] Calculate the number of frames per second the engine can do
- [X] Add best of calculation
- [X] Add basic stats
- Currently stored as :entitiy -> [:c1 :c2]
- Reverse it so that when an entity is created it is indexed to a direct lookup path i.e :component [:e1 :e2]
- Can use keys on :state -> :c1 to get all entities in one shot without a fitler then map
- Update to v3
- Add as a submodule
- Profiler says all the inner functions can not be optimized because the require a dynamic lookup and therefore can not be inlined by v8
- [X] Use macros to define a new function for each system/component instead of anonymous functions
- See branch defcomponent, didn’t pan out as this isn’t actually an issue on the first run
- [X] Game state helper Declare the function for a system, components and it will auto call mk-system/mk-component on them
- [X] Game loop helper
- Austin 1.6 has issues so need to use version 1.7-SNAPSHOT or the repl doesn’t work
- Had to update lein cljsbuild because it was to old to work with later cljs
- Now using the cljs required by core async latest
- https://github.com/clojure/clojurescript/wiki/Foreign-Dependencies
- No need to do this with externs
- Update the lookups of system/component fns inside the game loop to lookup by reference rather than by value??
- This appears to work now <2015-11-27 Fri>
- Use it to compare different implementations of the core game functions and optimize
- See chocolatier.engine.benchmarks for more
- [X] Figure out where we can use transients instead of normal hashmap operations
- Systems?
- Components?
- CES operations?
- [X] Change all systems to use transients
- [ ] Change collision detection to use transients of js arrays to do collision detection
- No longer need to do this since implementing rbush for r-trees
- Doesn’t seem to help that much, may revisit this <2015-11-27 Fri>
- Branch: async-systems
- Example:
- Given dependency tree [[a b] [a c] [c d]]
- Run in this order where a vector denotes async operation [a, [b c], d]
- Probably need to cache this when game loop is initialized and recalc anytime a new system is added/removed
- Turned out to be much slower due to overhead of core.async since these are all cpu bound tasks there is no benefit to async’ing them
Add direction to movement and animation so you remain in that direction when standing <2015-01-18 Sun>
- [X] Add stand as an action if not walking to Controllable
- [X] FIX animation stack keeps growing Need to remove the last action if a new one comes in
- State machine for representing animations
- Hold on to the last state so that after an animation it goes back to what it was in
- State
- Animation state key i.e. :walking :running
- Frame number (for sprite sheet)
- Dimensions of sprite sheet i.e width/height/frame-width/frame-height
- Abstraction for specifying an animation
- Key frame animation?
- Multi part sprites?
- [X] Renderable system should only call the stage render code
- [X] Move sprite updating based on movement to the animateable component
- [X] Can change to animateable system rather than renderable so all can share sprite stuff in one place. It only reacts to events so it’s ok
- Handles changes to sprites based on events including movement, animation, image swaps
- [X] Fix collision detection to use move component for position state or to get all info from the incoming event msg
- [X] Hold the hit zone info about an entity in the collidable component state
- [X] Position information should be held by the move component
- Looks like new events evaluation causes replay to not work
- One of the systems is clearing out messages before it can make it to the replay system
- Systems were seqing over a hashmap which is not guaranteed to have order
- Take a copy of game state every n seconds and stick in vector
- Add an input control for a button to control stepping backward
- Thumbnail???? Would be super cool to render a mini image
Update subscription calls to filter out messages properly using the passed in boolean function for determining if an even should go into an inbox
- Implement an AI behavior for entities with the :ai component
- Chose to go towards the player on each turn
- Alternatives
- Perform the lookup in one shot for all entities
- Sort by x, y, use x y to figure out which entities you should check against
- Spatial grid, divide up all the entities into a 2D grid once per frame, only compare entities in the same frame
- Cache the collision checks as you don’t need to compare every entity in reverse A->B AND B->A
- There was a message leak that was piling up in the queue
- Make event subscriptions opt in not opt out
- Subscribe to an event from someone to a specific ID
- Make broadcast subscriptions optional?
- Subscribe an entity to a specific event Subscribe a component? Subscribe an entity? What does the entity get in their inbox? All messages? When do you remove messages from inbox? Need better parsing of event messages
- Resulted in 2x framerate from 8-10 to 18-20
Put the messages in a hashmap instead of a list {:events {:subscriptions {} :queue {:<event-id> {:<source-id> [{:event-id :<event-id> :from :<from-entity> :msg <message>}]}}}} Subscribing to broadcast events is the concatenation of all values of keys nested in the event id A subscribed event is id -> from a specific entity
Don’t do a fan out as part of each system only check the events queue and make a lazy sequence that gets included as the inbox argument The event-system should clear out the event queue, handle new subscriptions/un-subscribes
- [X] Load tile set image
- [X] Load json
- [X] Translate spec into tile set
https://github.com/bjorn/tiled/wiki/TMX-Map-Format#tileset
- imageheight, imagewidth, tileheight, tilewidth, tileproperties
- The spec for the tiles is in data.layers[0].data and is a one dimensional array with numbers representing the tile to use
- To get the x, y of the tile
- number * tile width
- by the width of the image divided by
- [ ] Update tile system to display it Needs to read the offsets of the tiles to shift the tileset image by x and y
- core
- [X] game-loop
- systems
- [X] render
- [X] input Collects system input and stores it every loop
- [X] user control Does something with the user input
- [X] tiling
- [X] movement
Should handle reconciling user input to changes not sure how this is different than user control
- It’s different because without it you will not be able to check if you should make the next move resulting in a loop where you get stuck because you are always colliding
- Can the entity make it’s next move?
- Should take a message from input about changes and move if there is not also a message for a collision
- [X] collision detection Check against all entities to see if they are colliding
- [X] debug layer (draw circles around entities)
- Turn red when a message collision message is present in the inbox
- Not sure how to do this without coupling renderable, collidable, and debuggable
- Make the selector for entities check multiple component-ids to get entity ids
- Make custom component state parsing function
- [X] Repl changing of game state via state atom
- [X] Component/Entity events mailbox
- Each component should get access to it by default (can be nil)
- Example (send-msg state :from-component-id :from-entity-id msg)
- Messages are async, no response is given, all info must be in the message
- [X] Need a system to clear out messages
- [X] Fan out messages to all subscriber inboxes
- Read only component state if it is not yours
- [X] Default component functions should also take in an inbox as an argument by default
- [X] Provide a way of emitting event from any component by passing it in as an arg
- Currently, the component function returns a hashmap which will be merged in to the game state
- We also need to provide a way of conveying that an event(s) should be emitted
- Component functions can output 1 or 2 items
- If it’s 1 item then it’s the component state
- If it’s 2 items then it is component state and events
- You can never just return events
- [X] Update game system fns now that component fns return updated game state rather than component state No longer need to use deep-merge which is recursive and costly. Instead use iter-fns on the collection of component fns in a system
- [X] Clear events inboxes after the system runs each component function
- Currently, passing in a args-fn to mk-component-fn DOES NOT wrap the output of the function into a mergeable hashmap. The caller must handle it in the function. This is confusing since it is handled automatically if you don’t pass in an args-fn.
- 9 times out of 10 you will want to automatically merge in component state
- This will get even harder to manage yourself if we have to handle merging of events
- Solution:
- Allow optional argument parsing functions
- Calling the function with the desired arguments
- Wrapping the output of the function into something mergeable
- Allow optional argument parsing functions
[#A] Replace ces/deep-merge in systems with iter-fns since each component by default calls ces/update-component-state-and-events which returns an updated global state
- [X] Throw an error if output is not a 2 item collection
- [X] Throw an error if component state or inbox or event-fn etc are nil
- An entity should be able to implement it’s own function to satisfy a component
- Multimethod with a default should work perfectly here
- Example
Dispatch on the entity-id
(defn default-update-sprite
“Update the entities sprite”
[component-state entity-id]
(let [sprite (:sprite component-state)]
;; Mutate the x and y position
(set! (.-position.x sprite) (:pos-x component-state))
(set! (.-position.y sprite) (:pos-y component-state))
component-state))
(defmulti update-sprite (fn [component-state entity-id] entity-id)
(defmethod update-sprite :default [component-state entity-id] default-update-sprite)
- [X] Refactor to use a state hashmap which gets passed to all systems
- [X] Add tests for ces functions
- Specify your state dependencies in your component function
- Call your component function with the state in the order specified
- Return value must be a vector of all state to be merged in
- Example: (defcomponent stuff [input stage me] (my-fn input stage me)) Calls a fn to get deps out of state Takes the return result and makes it merg-able with global state
- Returns a defrecord with a hashmap of component Protocols and functions
- Reads :fields metadata of protocol and creates a list of all fields that will be the record’s state
- Creates a protocol
- Takes a name, state (hashmap of fields), and methods
- Returns a protocol with metadata about it’s fields
- time between game loop calls
- need to request the recur the loop
- [X] Change state to tile-map which is a hashmap with meta about the map and a list of Tile objects
FIX multiple reset-games makes the input move double as fast each time until the canvas is removed <2014-01-26 Sun>
- [X] Player needs to have a map position coordinate
- [X] Create a new component BackgroundLayer which is used by Tilemap to adjust it’s tiles based on players position
- [X] UserInput should set the direction and the x y offset based on velocity
- systems is not loaded on engine.core load
- This was due to compiled js being used instead
- the first time and requires calling start-game!
- there is a race condition where the renderer starts before the window has a width or height causing an error when pixijs renderer is called
- Need to load the asset for the tile background using an asset loader loader = new PIXI.AssetLoader([“resources/bg-far.png”,”resources/bg-mid.png”]); loader.onComplete = onAssetsLoaded loader.load(); Then one can simply wrap the code into the onAssetsLoaded function
Debug layer needs to be moved to engine/state so that it can be coordinated during resets <2014-03-22 Sat>
Collision detection for player causes the player to not be able to move (always colliding) <2014-03-14 Fri>
- Prevent movement if the result of the move is a collision
- The player is able to move such that the circles are overlapping
- Maybe the collision detection is wrong?
- The formula seems to work
chocolatier.engine.systems.collision> (collision? 352 220 20 322 186 30) true chocolatier.engine.systems.collision> (collision? 352 220 20 320 184 30) true chocolatier.engine.systems.collision> (collision? 352 220 20 317 181 30) false
- Maybe the addition of the offset x/y is not being checked properly?
DEBUG: Before offset 352 220 VM8682:9
DEBUG: After offset 356 220 VM8682:9
DEBUG: Before offset 306 206 VM8682:9
DEBUG: After offset 306 206 VM8682:9
DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 356 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9
DEBUG: State changed :input-debug {:A “off”} VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 352 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9
DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 352 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206 VM8682:9 DEBUG: Collision detected between :player and :monster VM8682:9 DEBUG: Before offset 352 220 VM8682:9 DEBUG: After offset 352 220 VM8682:9 DEBUG: Before offset 310 206 VM8682:9 DEBUG: After offset 310 206
- Maybe the order of the systems is wrong?
- Moved movement phase to right before the collision detection
- Render phase now applies the offsets calculated and applies them to the sprite
- !!! Player has an offset even though it’s screen position will not change
- Monster also has an offset based on the player’s offset
- [X] Use a global screen offset when user input comes in to control
- [X] Other entities then use the screen offset for their own movement and screen position
- Movement stop on collision works when both entities have collision detection on and the player is passed in to the collision check without any offsets
- Tiling works correctly when offsetst are removed but the monster moves with the player equally
- Debug log 1:
DEBUG: State changed :input
:W off -> on VM1052:9
DEBUG: State changed :global
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :monster
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :player
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :monster
:screen-y 328 -> 323
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :player
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :monster
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :player
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :monster
:screen-y 333 -> 328
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :player
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :monster
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :player
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :monster
:screen-y 338 -> 333
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :player
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :monster
:offset-y 5 -> 0 VM1052:9
DEBUG: State changed :player
:offset-y 5 -> 0 VM1052:9
DEBUG: Collision detected between :player 361 408 30 and :monster 365 343 40 VM1052:9
DEBUG: State changed :global
:offset-y 5 -> 0 VM1052:9
DEBUG: Collision detected between :monster 365 343 40 and :player 361 408 30 VM1052:9
DEBUG: State changed :monster
:offset-y 0 -> 5 VM1052:9
DEBUG: State changed :player
VM1052:9 DEBUG: State changed :monster
VM1052:9 DEBUG: State changed :player :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :global :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :monster :offset-y 5 -> 0 VM1052:9 DEBUG: State changed :player :offset-y 5 -> 0 VM1052:9 DEBUG: Collision detected between :player 361 408 30 and :monster 365 343 40 VM1052:9 DEBUG: State changed :global :offset-y 5 -> 0 VM1052:9 DEBUG: Collision detected between :monster 365 343 40 and :player 361 408 30 VM1052:9 DEBUG: State changed :monster :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :player
VM1052:9 DEBUG: State changed :monster
VM1052:9 DEBUG: State changed :player :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :global :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :monster :offset-y 5 -> 0 VM1052:9 DEBUG: State changed :player :offset-y 5 -> 0 VM1052:9 DEBUG: Collision detected between :player 361 408 30 and :monster 365 343 40 VM1052:9 DEBUG: State changed :global :offset-y 5 -> 0 VM1052:9 DEBUG: Collision detected between :monster 365 343 40 and :player 361 408 30 VM1052:9 DEBUG: State changed :monster :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :player
VM1052:9 DEBUG: State changed :monster
VM1052:9 DEBUG: State changed :player :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :global :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :monster :offset-y 5 -> 0 VM1052:9 DEBUG: State changed :player :offset-y 5 -> 0 VM1052:9 DEBUG: Collision detected between :player 361 408 30 and :monster 365 343 40 VM1052:9 DEBUG: State changed :global :offset-y 5 -> 0 VM1052:9 DEBUG: Collision detected between :monster 365 343 40 and :player 361 408 30 VM1052:9 DEBUG: State changed :monster :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :player
VM1052:9 DEBUG: State changed :monster
VM1052:9 DEBUG: State changed :player :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :global :offset-y 0 -> 5 VM1052:9 DEBUG: State changed :monster :offset-y 5 -> 0 VM1052:9 DEBUG: State changed :player
- The input handler can be on constantly and the user input step takes the latest value of the atom during a run through the game loop
- Player was able to perform an illegal move From (collision? 361 250 30 288 250 40) To (collision? 361 250 30 292 250 40) Even though they were colliding
- Maybe the collision detection is wrong?
- Player and monster are sharing the same offset-x and why which will negate each other during collision detection because the detector applies the offsets before checking. This makes it impossible to move away from a collision
- How movement works
- User input
- Player adds an offset based on user input
- Monster adds an offset based on the players offset
- Collision detection runs
- Offsets are removed if there is a collision
- Sprite positions updated based on applying offsets to screen position
- FIXED by removing offset of the player!
- [X] Collisions should be a core system run before movement
- [X] Entity collisions
- Collision based on velocity and direction
- Compare all entities to each other
- [X] Draw a circle for debugging in the render function chocolatier.engine.systems.debug
- [X] Update entity-collision? fn to use that instead of getting a sprite attribute. This will make it easier to test just by using a hashmap instead of a hashmap with a sprite object
- In the chrome js console profiler:
- “self” is how much time was spent doing work directly in that function.
- “total” is how much time was spent in that function, and in the functions it called.
- AI system profile
- apply is 45% of the time spent
- update-component-state-and-events 35%
- Looks like emit events is the culprite with update-in happening many times
- Broad collision system
- entities-with-multi-components 40% of time
- mk-spatial-grid is 52% of time, looks like the group-by is inefficient
- Narrow collision system
- Pretty small amount of time spent there
- narrow-collision detection takes 25%
- check-collsions appears to be the culprit with 20% time spent there
- get-subscribed-events takes 22% of the overall profiled time
- usage of mapcat appears to be the culprit with 20% time spent there
- Only looked at 2 systems movement and ai
- Major bottlenecks
- apply (unoptimized by v8) in component-fn-body
- emit-events reduce is spending lots of time and number of function calls will grow linearly with each entity added
- get-events spends all it’s time with get-in
- Start a game from scratch and build it up
- Show how to inspect game state
- Show what it’s like to work with
- Add a new system or feature
- Update an existing system on the fly
Takes a scene state and handler fn and returns a function
Takes a collection of scenes where the default scene is the first one Implements a fixed time step game loop Loads the stage (rendering engine) What happens if there is more than one defgame?
A collection of systems and initial state for managing the systems. Returns an atom with a hashmap representing the scene state. Returns a hashmap of functions for managing the scene such as state resets
- Metadata on def does not work in clojurescript
- Eval inside a macro resolving a symbol from another ns does not work
- Loading the caller’s ns does not work https://github.com/teropa/hiccups/blob/master/src/clj/hiccups/runtime.clj
- [ ] Bind to another ns in the macro http://stackoverflow.com/questions/7684656/clojure-eval-code-in-different-namespace
Creates a vector of actions used by an action list
Defines a new level for the game Takes a list of assets to load, which world map, game scripts to load etc
Pass a message from one entity to another and process the list of messages Processing the list of messages can result in generating actions on the action list Show grid lines with numbers based on the spatial grid of that frame It’s not working due to broad collision detection not accounting for mass of entity and not factoring in intended movement- Fix timestep loop causes rendering issues
- When you put the render system in iter-systems the movement is correct
- When it’s not you get inconsistencies in the rendering where certain parts are moved in weird ways like the background or monster
because render gets called outside of the systems step loop
- Tiles are moving but the Monster is
Line 73 in engine.core
This way the render step is only for updating draw code and we don’t have to be concerned with offsets
- [X] Combat system
- [X] Meta system [2/2]
Dynamically add/remove/change the game state by listening to events on the :meta where the message implements the specs for :entity
- [X] Add entity
- [X] Remove entity
- [X] TTL system/component When the duration of an entity exceeds the counter, emit an event to remove itself from the game
- [X] Attack component [2/2]
- [X] Attack throttling with configurable cooldowns
- [X] Emit events to :meta to create an attack entity
- [X] Implement cleanup functions for components
- [X] Update animateable component to remove the sprite from the stage using a :cleanup-fn
- [X] Update collision system to take into account offset-x/y Increase the size of the hit box to where the entity will be next frame this will mean all area the entity will pass through will be included for collision detection
- [X] Add damage system and hitpoint component
- [X] Emit damage events when colliding with an entity prefixed by “attack”
- [X] Emit an action event when colliding with an attack
- Update hitpoints
- [X] If hitpoints falls to 0 then emit a message to show the destro animation then remove the entity
- [X] Invinsibility period (cooldown of when they can take damage next)
- [X] FIX collisions with invisible bounding boxes?
- When eliminating some enemies, end up colliding with nothing
- [X] Determine if it’s attack collision if there is a damage attribute instead of overloading the entity-id
- [X] FIX cooldown requires a full cycle before it allows an attack
- [X] Systems should provide the lens for component functions instead of wrapping all the component functions
- [X] Update all the component state in one shot instead of updating the state for each entity->component
- Assumes there can be no inter system component state access or message passing
- [X] Figure out how to handle events Accumulate it using a transient and then emit it later
- [X] Update mk-system with less boilerplate for creating a system and an accompanying component
- Component’s are ALWAYS part of a system, a system does not always work on a component
- Avoid errors like not having a component function that is called by a system function
- Could present opportunities for optimization if we can guarantee the component fn is there
- API
- Custom system [:system :s1 my-system-fn]
- System with component [:system :s2 :c1 my-component-fn]
- [X] Update benchmarks
- [X] Remove mk-component-fn if it’s no longer used
- [X] Systems should still declare what components they work on so it’s not hardcoded into the implementation
- What about multiple components?
- Example: Collision detection works on entities that have movement and collision
- If they don’t work on a component then idiomatically use “global”
- What about multiple components?
- [X] Replace all args-fn with select-components
- What about when you need some global state like input?
- What if you need component state for a different entity?
- [X] Replace format-fn used by the ai system
- Probably want a custom system instead
- TODO need to remove the hardcoding of the component name
- [X] Update action rpg example
- [X] Update any systems that do anything special to implement their own system fn
- [X] Delete any systems that are generic
- [X] Update tests
- [X] mk-system-fn
- [X] Test events get emitted
- [X] Test component state gets updated
- [X] get-component-context
- [X] get-subscribed-events
- [X] mk-system-fn
- Currently, to get entities that have a component you would need to get all keys in that component’s state which is not very intuitive
- There should be a way of looking up all the components an entity has
- There should be a way of looking up all entities for a given component
- The index should be a set to guarantee uniqueness
- Instead of entity IDs being labels they can be types
- If they are known types then we can make component fns which are protocol methods
- This would allow you to create per entity ID polymorphism, i.e player1, some boss, have different movement rules?
- Would need a macro that copies all the functionality of the Keyword class so it can still be used for looking up things in the game state
- Issues:
- Dispatch of implementation would be per entity so if we want all enemies to share the same implementation would need a lot of copying
- Example:
(defprotocol Moveable (move [this component-state context] "Handle whether or not the entity should move")) (extend-type Player1 Moveable (move [this component-state {:keys [inbox]}] (let [{:keys [pos-x pos-y move-rate direction]} component-state {new-direction :direction} (:msg (get-move-change-event inbox)) collision? (collision-event? inbox)] (let [next-direction (or new-direction direction) [offset-x offset-y] (if collision? [0 0] (mapv #(* move-rate %) (direction->offset next-direction)))] (assoc component-state :pos-x (- pos-x offset-x) :pos-y (- pos-y offset-y) :offset-x offset-x :offset-y offset-y :move-rate move-rate :direction next-direction)))))
- Could also solve the validation problem so all keys are known up front
- In the benchmarks it didn’t make a difference
- Same benefit if you use transient or a js-obj
- Tested in benchmarks js objects and transient hashmaps performed 40% better that persistent maps
- In the running game engine though there was not much of a difference in the overall frame rate See the flatten-state branch for more
- [ ] Don’t return a new object, return the same one so we are not creating new data structures every time
- :cleanup-fn is not being called when an entity is being removed from the scene
- Can we make that automatically declared by the component instead of in the wiring up?
- Show the component states of an entity and be able to adjust it
- Implement as dom elements
- [X] Make game-loop take a state atom to copy state into every trip through the game loop
- [X] Auto generate form of component state selected
- [X] If something is changed, write the change into the game state
- This should happen in real time
- Maybe specify if you want to override the current component state with what’s in the inspector ignoring game loop?
- [X] Abstraction for adding functionality at the game loop level?
- This could be useful for tooling
- Logging
- Created middleware to address this
- [X] Remove the checkouts directory when done integrating
Alex Kehayias is the CTO at Shareablee and works on biggish data problems to help brands and publishers win the social web. Clojure and Python are his weapons of choice by day and by night. He is an active musician, tech community organizer (ClojureNYC), and helps mentor people learning to code.
Alex Kehayias shares the many joys and challenges of building a game engine in a functional style using Clojure and ClojureScript. Over the course of 2+ years, Alex has been actively writing (and rewriting) a functional game engine to find the ideal combination of a quick feedback loop, testing with data, and performance. Join in the sorrows and triumphs of bringing functional programming to web-based game development for fun and zero profit!
- Game loop as a reduce call Structuring the overall game loop as a reduction over a collection of functions into a data structure provides the benefits of functional purity, one way data flow, and easy testing.
- Entity Component Systems are a perfect match for FP Breaking down games following an entity component system popularized by Scott Bilas and the Unity game engine brings the ability to describe complex behaviors with data instead of code which makes for faster iteration to help find the fun.
- Functional programming comes at a cost Performance is always a concern in game programming and the additional usage of functional paradigms means additional overhead, but thankfully can be overcome thanks to the ClojureScript compiler and heavy reference of the core of Clojure.
- Functional Game Engine Design @ LispNYC: https://vimeo.com/152433890
- Clojure Panel @ ClojureNYC: http://www.meetup.com/Clojure-NYC/events/224632241/
- Storm & Clojure: http://www.meetup.com/Clojure-NYC/events/219336391/
- And a few other workshops as part of the ClojureScript NYC meetup group
- Send a query with what you want and a filter function
- Returns a sequence
- Resolved: components have an option to select component state that will be included in their context
- [ ] Limit on number of messages in an inbox
- [ ] Limit on number of messages in the event queue
- Ended up adding a system as the last system in the scene that wipes all messages at the end of each loop
- Fixed timestep animation? Each frame ticks the next frame in an animation
- Action list animation? This would allow the cancellation of an animation easily. Say a player is attacking and then get’s hit halfway through the animation, could cancel the animation and start the hit animation
- Use loops instead of for/map as they are more efficient when using transients or arrays as accumulators