0
votes

I have a vector of entities (maps), I have filtered this vector to those entities with a particular key. Then I apply a function to update some values in those entities, and now have a subset that I'd like to merge with the original superset.

world     [{a} {b} {c} {d} {e}]
a         [{b} {d} {e}]            ; (filter matches? world)
b         [{b'} {d'} {e'}]         ; (map func a)
new-world [{a} {b'} {c} {d'} {e'}] ; (merge world b)

How can I merge my update b with the original world?

My map structure is:

{:id identity
 :attrs {:property1 {:param1 value
                     :param2 value}
         :property2 {}}

There can be a variable number of properties. Properties can have 0 ({}) or more params. The only part of the map that is changing are the values, and only some of them.

I have to peer into each map to identify it, and then merge accordingly? What is an idiomatic way to do this?

Clojure's map function can take multiple sequences, but won't compare my subset b to the whole superset world, only b number of world entities, and stop once b is exhausted.

> (map #(str %1 " " %2) [1 2 3 4 5] [6 7 8])
("1 6" "2 7" "3 8")

Have I chosen the wrong structure for my data in the first place? Would I be better off without a vector and just one map?

{:entid1
   {:property1 {:param1 value :param2 value}
    :property2 {}}
 :entid2
   {:property1 {:param1 value :param2 value}
    :property2 {:param1 value}
    :property3 {:param1 value :param2 value :param3 value}}}

This question looks similar but does not merge my vectors correctly.


Real world implementation

My actual code is part of a game I'm writing to familiarise myself with Clojure (ClojureScript in this instance).

The game state (world) is as follows:

[{:id :player,
  :attrs
    {:gravity {:weight 10},
     :jump {:height 60, :falling false, :ground true},
     :renderable {:width 37, :height 35},
     :position {:x 60, :y 565},
     :walk {:step 4, :facing :right},
     :input {},
     :solid {:blocked false}}}
 {:id wall1,
  :attrs
    {:solid {},
     :position {:x 0, :y 0},
     :renderable {:width 20, :height 600}}}
 {:id wall2,
  :attrs
    {:solid {},
     :position {:x 780, :y 0},
     :renderable {:width 20, :height 600}}}
 {:id platform3,
  :attrs
    {:solid {},
     :position {:x 20, :y 430},
     :renderable {:width 600, :height 20}}}] 

I update the world on each animation frame, then re-render the canvas.

(defn game-loop []
  (ui/request-frame game-loop)
  (-> world
      system/update!
      ui/render))

system/update! is:

(defn update! [world]
  (let [new-world (->> @world
                      (phys/move @player/input-cmd))]
    (player/clear-cmd)
    (reset! world new-world)
    @world))

My plan was to have a chain of systems updating the world in the thread last macro. I am in the process of writing phys/move.

This means that systems have to update the world, instead of just returning their effect on the world (vector of changes).

I'm contemplating if it's more manageable to have the system/update! function manage the effects on the world (applying the updates). So systems only return their list of changes. Something like:

(defn update! [world]
  (let [updates []
        game @world]
     (conj updates (phys/move @player/input-cmd game))
     (conj updates (camera/track :player game))
     ; etc.
     (reset! world
       (map #(update-world updates %) world))
     @world))

Repeated (conj updates (func world)) feels clunky though. This is possibly more work than just merging updates (or returning an modified entity) in a system function.

How to elegantly pass state changes between my system functions (phys/move, camera/track), and subsystem functions (walk, jump)?

My stab at applying Joaquin's map only approach to my move system:

(ns game.phys)

(def step 4)
(def height 50)

(defn walk?
  "is this a walk command?"
  [cmd]
  (#{:left :right} cmd))

(defn walks?
  "update? equivalent"
  [entity]
  (contains? (:attrs entity) :position))

(defn walk
  [cmd entity]
  (if (walks? entity)
    (let [x (get-in entity [:attrs :position :x]
          op (if (= cmd :left) - +)
          update {:attrs {:position {:x (op x step)}
                          :walk     {:facing cmd}}}]
      (merge entity update))
    entity))

; cmd is a deref'ed atom that holds player input commands
; e.g. :left :right: :jump
(defn move
  [cmd world]
  (cond
    (walk? cmd) (map #(walk input-cmd %) world)
    (jump? cmd) (map #(jump height %) world)
    :else world))
1
The only doubt I have around the single map approach, is filtering entities prior to updating them. I can filter a collection but it's more work with a single map?Greg K
Using a map, you can generate a filtered sequence of keys to the movable entities. (When you use a map as a sequence, it presents itself as a sequence of [key value] pairs.)Thumbnail
A small point: the when-let in function move will always succeed - () is truthy. You probably want to wrap the expression for movables in a seq.Thumbnail
Thanks, I did end up doing that.Greg K
In the light of your update, @Jouquin's is the correct answer. I'm afraid I got hold of the wrong end of the stick.Thumbnail

1 Answers

1
votes

It is simpler than all that (having a vector of maps is something pretty common, and it may fit your problem properly). Instead of doing a filter and then a map, just do a map:

(defn update? [entity] ...)
(defn update-entity [entity] ...)

(defn update-world [world]
  (let [update #(if (matches? %) (update-entity %) %)]
    (map update world)))

This pattern of updating something or just leaving it as it is is pretty common, and it is idiomatic to make the functions that update something return the updated thing, or the old thing if it did nothing, so at the end it would be something like:

(defn update-entity?
  "Predicate that returns if an entity needs updating"
  [entity] ...)

(defn update-entity
  "Updates an entity if it is needed.
   Otherwise returns the unchanged entity"
  [entity]
  (if (update-entity? entity)
    (assoc entity :something :changed)
    entity))

(defn update-world [world]
  (map update-entity world))

You can see some examples of this behavior in the game logic of this snake game:

https://github.com/joakin/cnake/blob/master/src/cnake/game.cljs#L54-L66
https://github.com/joakin/cnake/blob/master/src/cnake/game.cljs#L102-L114