Fork me on GitHub
#hoplon
<
2017-05-23
>
cpmcdaniel21:05:33

@alandipert - hacking on this holpon rogue-like (calling it “Conjure”)

cpmcdaniel21:05:39

I’m starting to get the feeling that I will need to take my world state and drop it down to plain JS, and have my rendering system sync that up with a javelin cell for view state

cpmcdaniel21:05:51

things like the movement system require lots of iterations to find the entities at the position it is moving to and determine if they are passable, etc.

cpmcdaniel21:05:15

similar for combat system

cpmcdaniel21:05:27

I could index entities by position… but then I have to coordinate the update of two pieces of state

mudphone22:05:49

cpmcdaniel: n00b here, but could you use the dosync macro on multiple cell swaps?

cpmcdaniel22:05:32

does dosync work on cells?

mudphone22:05:11

Yeah, I just read that on the Javelin site and saw it in a talk video: https://github.com/hoplon/javelin#transactions

cpmcdaniel22:05:58

up to this point, I have been treating my world state as one big cell, with the view using cell= formulas for each “block” on the 2d svg screen

cpmcdaniel22:05:40

I then have a main loop that responds to keypress. Then a chain of systems update the world state.

cpmcdaniel22:05:37

So (swap! world #(process-command (input/do-input % (.-key event))) does all the work

cpmcdaniel22:05:55

and process command is doing things like moving the player, etc.

mudphone22:05:28

I see, perhaps another way to get at two parts of the world state (in a single cell) is via Lenses?

cpmcdaniel22:05:58

so I don’t think I need dosync. My main concern is that each subsystem (movement, combat, magic, etc) will have to iterate over lots of entities

cpmcdaniel22:05:44

so I was pondering making the world state mutable JS arrays & objects, and having a system that syncs that to the view state in one swap! call

cpmcdaniel22:05:05

this is fortunately turn-based, so rendering can happen last

cpmcdaniel22:05:12

looking at lenses

mudphone22:05:14

JS array iteration is supposed to be fast, iirc

cpmcdaniel22:05:27

oh, can you pass JS arrays straight to (map) and others that take seqs?

mudphone22:05:28

I’m not totally sure if this helps you, but this is what I was referring to (I think, it’s been a while): http://swannodette.github.io/2013/06/10/porting-notchs-minecraft-demo-to-clojurescript

mudphone22:05:32

Sorry, I’m not totally clear on what you really need, and don’t want to send you on a wild goose chase.

mudphone22:05:12

When you mentioned “iterating” through a bunch of things, I remembered this tidbit.

thedavidmeister23:05:14

@U0564EGNY is the problem just that you want control over when/how calculations are run?

cpmcdaniel23:05:20

I have control, its just that there are a lot of entities to iterate through, and some systems will need to iterate through them multiple times

cpmcdaniel23:05:28

So here is a bit of what my world structure looks like, if you can imagine…

cpmcdaniel23:05:42

An entity is just an ID, so lets say the player is entity 0

cpmcdaniel23:05:36

world has an :etoc key that is a vector of maps. Each map is a mapping of component name to component data for the entity at that index. Example:

cpmcdaniel23:05:10

(get-in world [:etoc 0 :position]) => {:x 1 :y 2}

cpmcdaniel23:05:26

things that move have a :velocity component

cpmcdaniel23:05:08

so to do movement, you have to find all things with :velocity component and in order to move them, you then have to iterate over everything with a :position component to avoid collisions

cpmcdaniel23:05:16

else you would walk right through a wall

cpmcdaniel23:05:04

now, if you have N monsters and a map that is 198 x 66, you have a lot to iterate through

cpmcdaniel23:05:34

now, I also store :ctoe in world, which is a mapping of component name to a set of entity ids

cpmcdaniel23:05:44

so that helps

cpmcdaniel23:05:05

but still, for collision detection, it’s a lot of unnecessary iteration

cpmcdaniel23:05:33

so I could store a :positions mapping in the world and keep it in sync. Otherwise I will be iterating over pretty much every entity in the dungeon when there are at most 8 I really need to look at

cpmcdaniel23:05:55

N x 198 x 66 scares me

cpmcdaniel23:05:17

but, it’s how a lot of games are designed

cpmcdaniel23:05:25

lots of looping

cpmcdaniel23:05:22

so the trade-off is more memory with a hash-map index

cpmcdaniel23:05:41

or drop to JS

mudphone23:05:06

what if you added the entity id, and velocity to the position map. (making it a “state” map) for an entity

mudphone23:05:31

Then you could filter a set or vector of entities by presence of attributes (such as velocity), and it would take a single pass

cpmcdaniel00:05:32

actually, the position map would actually be a 2D vector (rows and colums)

cpmcdaniel00:05:49

so you would index into by (x, y) coordinates.

cpmcdaniel00:05:13

and the values would be a set of entity IDs

cpmcdaniel00:05:55

because some entities stack

cpmcdaniel00:05:31

for example: floor <- scroll <- monster

cpmcdaniel00:05:45

all can occupy the same spot on the grid

cpmcdaniel00:05:07

actually, might use a vector of entity IDs to keep them in the right stacking order

mudphone00:05:37

om uses that denormalized data thing, with the parser

mudphone00:05:49

maybe use something like that with an index?

mudphone00:05:25

although, I would probably prefer putting entity objects in a set or vector and filtering them, or directly looking them up via a map

cpmcdaniel00:05:32

I wonder if I am going to get into other situations like this (wanting to optimize) when I get into other systems like ranged combat

cpmcdaniel00:05:06

there you will want to probably start with all entities that have the :health component

cpmcdaniel00:05:59

(get-in world [:ctoe :health]) => #{0 4 5 12}

cpmcdaniel00:05:22

but then you have to do line of sight checks

cpmcdaniel00:05:08

the position optimization is probably a good trade-off though

cpmcdaniel00:05:33

because rendering requires almost every entity

cpmcdaniel00:05:54

also, so many things besides rendering require knowledge of position - combat, range combat, line of sight, monster AI

cpmcdaniel00:05:55

I should also state that this is not graphically complex

thedavidmeister00:05:10

@U0564EGNY is this logic something that could be handled with datascript queries?

thedavidmeister00:05:39

not that it magically improves the speed of a loop, but it has some things like internal indexes that could help

thedavidmeister00:05:53

sounds like a good idea to have a bit of a "cache" of entities that are actually close enough to matter

cpmcdaniel00:05:20

yeah, would make for less code, certainly

cpmcdaniel00:05:34

but I’d be worried about performance

thedavidmeister00:05:54

the very first thing you could do would be run filter over your entities, with a distance calculation function

thedavidmeister00:05:15

so it's one big, but relatively fast, iteration up front each turn, but everything else is quick

cpmcdaniel00:05:16

I guess there are lots of ways to slice this. I’ve been taking my inspiration from other rogue-likes and comonent-entity-systems in general from game design

cpmcdaniel00:05:52

the ones in C actually use a bitmask array of length = max_entities

cpmcdaniel00:05:02

the bitmask tells you what components the entity has

thedavidmeister00:05:23

i don't know about performance

thedavidmeister00:05:41

but datascript seems like a pretty good fit for keeping track of attributes of game entities

cpmcdaniel00:05:48

then for each component you have an array (also max_entities), and the data for each entity at the appropriate index

cpmcdaniel00:05:00

I might play with it a bit

thedavidmeister00:05:14

it's a datomic clone in cljs

cpmcdaniel00:05:24

yeah, that much I get

thedavidmeister00:05:24

if you use namespaced attributes like :position/x and :monster/hp i imagine it should be relatively easy to keep track of what's what

cpmcdaniel00:05:36

so those component arrays end up being sparse, but array iteration in C is so fast

cpmcdaniel00:05:21

hmm, I didn’t think to use the namespace of the keyword as the component indicator

thedavidmeister00:05:56

well then it fits with spec

cpmcdaniel00:05:32

well, ant that is how datomic treats things: entity-attribute-value

thedavidmeister00:05:46

i just checked, datascript implements 3 of the datomic indexes

thedavidmeister00:05:46

EAVT, AEVT and AVET indexes

thedavidmeister00:05:08

so you don't get VAET

thedavidmeister00:05:42

but i think you want AVET anyway

thedavidmeister00:05:37

datomic at least says "The AVET index also supports the indexRange API, which returns all attribute values in a particular range."

cpmcdaniel00:05:51

yeah, give me all things that have a position component, which I could use either :position/x or y for

thedavidmeister00:05:16

you could just create ranges for x and y, basically a "bounding box"

cpmcdaniel00:05:19

I wonder if transact! works on javelin cells

thedavidmeister00:05:13

just need to add a little bit of meta data to the cell

thedavidmeister00:05:57

well, even more than just "give me everything with a position"

thedavidmeister01:05:13

you could say "give me everything with a position within this area"

cpmcdaniel01:05:47

thanks, I’m going to play with this

cpmcdaniel01:05:57

… and just when I had basic movement working!

thedavidmeister01:05:21

hah, i spend way more time reworking than working

cpmcdaniel01:05:23

well, half the purpose of this was to learn.

cpmcdaniel02:05:51

@U0D4G0Q4U ok, so how do you create a javelin formula cell that queries the db cell?

thedavidmeister02:05:38

@U0564EGNY you don't, you query db values, not cells

thedavidmeister02:05:41

here's a random example

thedavidmeister02:05:44

(defn entity->list-title
 [e]
 (let [list-id (-> e :item/list-id :db/id)
       db (d/entity-db e)]
  (when list-id
   (first
    (d/q '[ :find [?t]
            :in $ ?id
            :where  [?e :item/list-id ?id]
                    [?e :item/list-title ?t]]
          db
          list-id)))))

thedavidmeister02:05:43

ah soz, doesn't actually have a formula cell in that one 😛

thedavidmeister02:05:08

(let [ids-titles (j/cell=
                   (into (sorted-set)
                    (d/q
                     '[:find ?lid ?t
                       :where [?e :item/list-id ?lid]
                              [?e :item/list-title ?t]]
                     conn)))]

cpmcdaniel02:05:32

(def test-var (cell= (into [] (d/datoms (d/db dw) :avet :position/y 3))))

cpmcdaniel02:05:39

dw is my conn cell

thedavidmeister02:05:30

you don't need the d/db

thedavidmeister02:05:41

formula cells deref other cells, and dw is a cell

thedavidmeister02:05:55

(d/datoms dw :avet :position/y e) should be fine

cpmcdaniel02:05:48

#object[Error Error: Assert failed: (db/db? db)]

cpmcdaniel02:05:34

I got a regular query to work

thedavidmeister02:05:55

dw should be a db already because of the formula cell wrapping it

cpmcdaniel02:05:58

(def test-var (cell= (into [] (d/datoms dw :avet :position/y 3))))

thedavidmeister02:05:28

(h/if-tpl (j/cell= (d/datoms conn :eavt))
     (el.project-list.dom/project-list :conn conn)
     (h/div :class "welcome"
      (h/span :class "empty-text"
       "You don't have any projects yet! "
       (h/br)
       (h/a
        :click #(project.wire/+! conn)
        "Create a new project")
       " to get started."))))))

thedavidmeister02:05:27

this is how i build my conn cells

thedavidmeister02:05:30

(defn conn-cell
  "Builds a fresh conn cell wrapping an empty db"
  ([]
   (conn-cell {}))
  ([schema]
   {:pre  [(map? schema)]
    :post [(d/conn? %) (j/cell? %) (= {} (-> % meta :listeners deref))]}
   (conn-cell-from-db (d/empty-db schema))))

thedavidmeister02:05:19

do you have a schema that supports the :avet indexing?

cpmcdaniel02:05:42

(def dw (conn-cell {:position/x {:db/index true}
                    :position/y {:db/index true}}))

cpmcdaniel02:05:08

also get the error with :eavt

thedavidmeister02:05:12

ok cool, so you have db/index set

thedavidmeister02:05:36

what if you just prn dw?

thedavidmeister02:05:07

what does (d/conn? dw) and (d/db? <@U04VCMKQN>) return?

cpmcdaniel02:05:31

(def test-var (j/cell= (d/q '[:find ?e :where [?e :position/x _]] dw)))
@test-var

cpmcdaniel02:05:21

some sort of bug?

cpmcdaniel02:05:55

#object [javelin.core.Cell <#C07V8N22C|datascript>/DB {:schema #:position{:x #:db{:index true}, :y #:db{:index true}}, :datoms [[1 :appearance/symbol "@" 536870913] [1 :health/current 75 536870913] [1 :health/max 100 536870913] [1 :player/name "Frodo" 536870913] [1 :position/x 1 536870913] [1 :position/y 2 536870913] [1 :position/z 100 536870913] [1 :velocity/x 0 536870913] [1 :velocity/y 0 536870913] [2 :appearance/symbol "." 536870913] [2 :position/x 1 536870913] [2 :position/y 3 536870913] [2 :position/z 0 536870913]]}]

cpmcdaniel02:05:09

(into [] (d/datoms <@U04VCMKQN> :avet :position/x 1))
works

cpmcdaniel02:05:46

so something with the way d/datoms and cell= are interacting

thedavidmeister02:05:53

what versions of datascript, javelin and cljs are you using?

thedavidmeister02:05:24

i don't understand how (d/db? <@U04VCMKQN>) could be true, but then (j/cell= (d/db? dw)) would be false

thedavidmeister02:05:44

that might be out of my ability to debug remotely >.<

cpmcdaniel02:05:40

let me just try that much

cpmcdaniel02:05:15

ok, that works

thedavidmeister02:05:59

but that's all d/datoms is doing internally

cpmcdaniel02:05:25

ok, it’s working now

cpmcdaniel02:05:39

must have had things in a funky state

thedavidmeister02:05:05

also, if you just want positions, you can look at making a filtered db and running your queries against that

cpmcdaniel11:05:54

I think > 90% of entities will have position

cpmcdaniel11:05:50

So, the one main concern I have with using datascript for this is that, previously, my whole turn-loop was one big atomic cell swap operation. And that works really well because it is turn-based. With datascript, I don’t see how I could avoid doing lots of little transactions within a single turn, causing lots of cascading “change events” to dependent cells

cpmcdaniel11:05:19

I can’t simply queue up transactions for the end of turn. Consider movement. What if 2 entities want to move into the same space?

cpmcdaniel11:05:10

I suppose I could sort them by priority and use a db function to avoid collisions, but then we’re getting rather complicated

cpmcdaniel11:05:43

perhaps I could just use the datalog parts of datascript to query my own custom state cell (the one I have now)

cpmcdaniel11:05:14

…or… I could copy the db at the start of the turn and swap it after (sort of like react with the vdom)

mudphone21:05:39

A lot of what you’re describing sounds like “game logic.” In which case perhaps you don’t want it in Datascript queries? Since it sounds like you’re already building that into a library of Javelin cells. Just a thought.

cpmcdaniel21:05:02

yeah, Datascript is very similar to what I want in terms of how data is represented (entity/attr/val)

cpmcdaniel21:05:16

but there’s some other things that make it not quite a great fit

thedavidmeister01:05:32

@U0564EGNY you can stop the propagation

thedavidmeister01:05:43

propagation only happens when vals change

thedavidmeister01:05:16

so put an "end of turn" cell in between your db and the downstream calculations

thedavidmeister01:05:10

one sec, i'll put together an example

thedavidmeister01:05:00

(defn turn-cell
 "Returns a lens that always updates to a new random-uuid when swapped/reset"
 []
 (let [t (j/cell (random-uuid))]
  (j/cell= t #(swap! t random-uuid))))

(defn end-of-turn-cell
 "Given a cell and a turn cell, returns a cell that syncs with the given cell only when the turn updates"
 [c turn]
 {:pre [(j/cell? c) (j/cell? turn)]}
 (j/with-let [c' (j/cell nil)]
  (h/do-watch turn #(reset! c' @c))))

; Example usage
(let [conn (conn-cell)
      turn (turn-cell)
      ; the lens means when we update turn to anything it just result in a random-uuid anyway.
      end-turn! #(reset! turn true)
      turn-conn (end-of-turn-cell conn turn)]
 (transact! conn [{:monster/id "turtle" :position/x 1 :position/y 5}]) ; conn propagates
 (transact! conn [{:monster/id "fish" :position/x 3 :position/y 10 :position/z -1}]) ; conn propagates
 (end-turn!)) ; turn-conn propagates

cpmcdaniel01:05:39

hmmm….still planning to test 3 methods: custom world cell, datascript, and pure js world object

cpmcdaniel01:05:10

in the latter case, I would update a cell at the end of each turn from the js object

thedavidmeister01:05:00

@U0564EGNY this technique isn't specific to datascript

thedavidmeister01:05:23

the point is that your concept of a "turn" is now explicit in data somewhere - it has a uuid

cpmcdaniel01:05:52

yeah, but also I’m not sure I want every tile being a formula cell either

thedavidmeister01:05:53

so you can watch and respond to that directly

cpmcdaniel02:05:19

I’m thinking I keep a 2D vector of input cells and only update the ones that need to change

thedavidmeister02:05:22

why does every cell have to be a formula cell?

thedavidmeister02:05:54

why does moving the "turn" into a uuid have anything to do with that?

cpmcdaniel02:05:25

at the end of turn the view must be updated

thedavidmeister02:05:31

my point was just that if you try to co-ordinate "end of turn" with callbacks or queues or whatever else

thedavidmeister02:05:36

it becomes a structural thing

thedavidmeister02:05:10

but if "current turn" is data, then sure, you can build callbacks and whatever on top of that

cpmcdaniel02:05:24

yeah, I think a queue of which tiles to update might be most efficient

thedavidmeister02:05:32

but you have an explicit thing that can be referenced, rather than an implicit emergent behaviour that needs to be managed

thedavidmeister02:05:59

efficient to what?

thedavidmeister02:05:14

execute? think about? write? refactor later?

thedavidmeister02:05:11

also, i don't see how having a queue is mutually exclusive to tracking the current turn?

thedavidmeister02:05:34

wouldn't you just put a flush-queue! function on a do-watch for the current turn?

thedavidmeister02:05:18

and also, if you have a queue anyway, why can't the queue be a vector to transact into datascript in bulk?

thedavidmeister02:05:11

@U0564EGNY soz, it's a little tricky because i can't see your code >.<

thedavidmeister02:05:59

i'm asking like a million questions 😛

cpmcdaniel02:05:04

yeah, I’ll put it up on github at some point

cpmcdaniel02:05:18

but right now I’m throwing lots of darts

thedavidmeister02:05:30

yeah, i know that feeling 🙂

cpmcdaniel13:05:11

my preliminary tests (no datascript yet) indicate low-level js performance is about 100x faster than cljs for creating world state, and about 20x faster searching for entities by position.

cpmcdaniel13:05:56

I’ll post my code to github this weekend and we can tear it apart

cpmcdaniel13:05:18

I need to comment it a bit so intent is clear

thedavidmeister04:05:59

@U0564EGNY i'm not surprised really, lower level tools are often faster

thedavidmeister04:05:12

@U0564EGNY the question is always "fast enough?" not "fastest?"

thedavidmeister04:05:08

for a turn based game, i'd imagine (without knowing much of the details) that you could get away with a few hundred ms, or even a few seconds of "thinking" per turn

thedavidmeister04:05:13

which is a lot of wiggle room

cpmcdaniel21:05:17

@micha might pick your brain about this on Thursday

dm321:05:30

I’d guess that’s exactly what cells were made for 🙂 you could have the position index happen automatically

cpmcdaniel22:05:04

continuing this conversation in thread ^