paldepind / Kran

An entity system written in JavaScript.
MIT License
42 stars 5 forks source link

Grouping Behavior May Be Insufficient for Some Systems #1

Closed VincentToups closed 10 years ago

VincentToups commented 10 years ago

Hi,

I really like Kran - I've built a nice piece of generative art with it that you can see here:

http://procyonic.org/clocks/clock10.html

(put it in a separate window and let it run for ~20 minutes)

In previous pieces like the clock above, I've found it useful to start several clocks at the same time on the same page to see how quickly they diverge from one another due to non-linear dynamics. However, kran seems to be designed under the assumption that a given page have only a single kran "universe" running - groups allow you to to have finer grain control over when particular systems are executed and how often, but they don't seem to cover entities.

That is to say, that if I want to getEntityCollection with a given set of components I cannot easily set up two distinct kran universes with different collections of entities comported from the same set of components.

Ideally, this would require that one be able to create multiple kran instances, each with their own collection of entities and systems so that multiple kran systems could run in one page, but this functionality doesn't seem apparent.

eg:

kran1 = new Kran() c1 = Kran.component(...) kran1.entity().add(c1)...

kran2 = new Kran() kran2.entity().add(c1)...

kran1.all() kran2.all()

Importantly, I want systems defined in kran1 to not effect or know about entities defined in kran2.

Perhaps I am missing a feature of groups that might allow me to perform this sort of operation?

One work around is to create an empty component for each effective simulation and give each entity an appropriate "kran system" component. I am unable to evaluate the performance implications of this approach - any hints?

paldepind commented 10 years ago

Hi VincentToups!

First of all: Thanks a lot for using Kran. It's very rewarding for me to see my library being used by other than myself. Your clock is running in another tab - I'm exited to see how it turns out. And great job figuring out how to use it with the currently sparse documentation!

You are spot on when claiming that Kran was created under the assumption that you'd never need more than one Kran "universe". It's something I did consider but I ended up concluding that it was unnecessary. That was simply due the fact that I couldn't think of a use case where one would need that. But now you've provided one so this feature will be added in.

I need to be sure though what exactly you mean when you say "Kran universe". You mention that only systems have groups. That's because entities are grouped according to their components. Say you want to have two clocks on the same page. Then one could create a component describing which one of the two clocks a given entity belongs to:

var clock = component("nr")
var entityOnClockNr1 = entity().add(clock, 1)
var entityOnClockNr2 = entity().add(clock, 2)

This would be just another component. I.e. it wouldn't add a performance penalty. This appears to be what you mention as a work around but I don't think it necessarily is.

Alternatively one could change Kran so that new Kran creates a new Kran "universe" completely separate from any other Kran "universe" that may or may not exist. This means that neither components, systems nor entities would be shared. If your two Kran universes share the same systems and components they'd have to be duplicated anyway. In any case I think that is a nice feature to have - so I'll add it.

VincentToups commented 10 years ago

So I have a partially completed new project that does just as you suggest here, which is to create a dummy component to distinguish between the different kran universes. This is a fairly satisfactory solution except for the minor issue that kran.all, for instance, still updates all systems in all kran universes, so multiple instances of a simulation class that depends on kran need to coordinate their execution of kran.all. One possibility is to further restrict each simulation's systems to separate groups. Then each running simulation, in addition to adding the stub component to all of its entities marking them as belonging to that simulation, would also place all of its systems in distinct groups. Then each universe would be separate from each other universe but they would be allowed to share components.

This seems like a reasonable compromise, but it sort of feels like abusing the various features of kran to simulate just having multiple kran instances.

Is there an issue with having components be global while having various, separate kran systems?

VincentToups commented 10 years ago

PS - thanks for the quick reply. I know this is an unusual use case, so I appreciate the open-mindedness!

paldepind commented 10 years ago

So I have a partially completed new project that does just as you suggest here, which is to create a dummy component to distinguish between the different kran universes.

I wouldn't call it a dummy component. It contains information about what universe an entity belongs to - and storing information is pretty much what components are about.

Is there an issue with having components be global while having various, separate Kran systems? So you're suggesting that components should be shared between different Kran instances but not entities or systems? I think that is inconsistent and would confuse users. If we add support for multiple Kran instances everything should be kept separate - that would be the most logical.

But, if Kran instances exposed their otherwise internal list of components one could do this:

var world1 = new Kran()
var world2 = new Kran()
world1.components = world2.components
var comp = world2.component( // bla bla

Then the to Kran instances would share the same component array and thus they would share components. Is that an adequate solution for you?

VincentToups commented 10 years ago

The solution you've suggested seems pretty acceptable, and is pretty close in functionality to the thing I've hacked up in the mean time:

I've actually implemented a solution already in the form of a class which exposes an interface similar to that of Kran but which automatically creates a unique component on instantiation of a class instance, and whose entity method automatically adds that component to all entities, and whose system method automatically manages the systems into groups and sub-groups unique to the instance, such that invocations of all on the instance refer to a unique kran-group and systems created with explicit groups are given hidden unique names. In keeping with the idea of totally encapsulating the universe, systems definitions do not refer to the universe component in their every,post and arrival methods. There is some machinery to drop the initial component before calling each method.

The source code is unfortunately (for the purposes of inter-developer communication) in a personal dialect of Lisp which I am developing which compiles to Javascript. If you are interested I could show the code?

paldepind commented 10 years ago

It sounds pretty cool! But you could propably have patched the funcitonality into Kran in the same time ;)

Yes. I'd like to see the code. I did check out the source for Clock 10 so I've already met your Lisp dialect! I actually tried to find a List to JavaScript language a few weeks ago. I was looking fo one that was just JavaScript in Lisps clothing if that makes sense. I.e. I wanted it to compile 1:1 to JS (so one could use all JS libraries) but still allow for macros etc.

I'll take a closer look at gazelle later. But you should now that I'm about to go on a holiday for a week. I'll be happy to add support for the features you've requested when I get home but I don't have time for it right now unfortunately.

VincentToups commented 10 years ago

So Gazelle is supposed to be as close to Javascript as possible while still meaningfully providing Lisp-like features, but it is in a sort of weird place right now because I want to port it into itself and I don't really have time. The generative art stuff is bite-sized, so I can fit it into my nights and weekends. But a port of Gazelle over to Gazelle (it is currently written in Emacs Lisp, because I use emacs for everything anyway) would take a weekend or two of dedicated work.

You can see Gazelle code by just changing the file extension for a script on my site to ".gazelle", eg:

http://procyonic.org/clocks/scripts/clocks/clock10.gazelle

Gazelle uses require.js for modular programming support, but that means that macros and patterns, which are extensible in gazelle, must also support scoping to modules. It is kind of complicated, and the system needs a total rewrite, which I'd rather do when I port Gazelle to Gazelle.

Anyway, here is the code to the class which implements a sort of "universe" functionality, backed by kran:

(module 
 (("hooves/hooves" :all)
  ("hooves/class-utils" :all)
  ("maria+/html" (:with-prefix <> :all))
  ((js-library "kran" kran)))

 (_include-js "./track-transforms.js")
 (_include-js "./array-rotate.js")

 (Object.defineProperty
  window.CanvasRenderingContext2D.prototype
  'excursion
  {get 
   (lambda () 
     (lambda (block)
       (this.save)
       (var result undefined)
       (try 
        ((set! result (block.call this this)))
        finally
        ((this.restore)))
       result))})

 (define+ pi2 
   (* Math.PI 2))

 (define+ pi/2 (/ Math.PI 2))

 (define-macro+ loge ((tail body))
   `(console.log ,@(loop for item in body append `(,(format "%S" item) ,item))))

 (define+ (discrete-hour-angle d)
   (var h (% (.. d (getHours)) 12))
   (_- pi/2 (_* pi2 (/ h 12))))

 (define+ (discrete-minute-angle d)
   (var m (.. d (getMinutes)))
   (_- pi/2 (_* pi2 (/ m 60))))

 (define+ (discrete-second-angle d)
   (var s (.. d (getSeconds)))
   (_- pi/2 (_* pi2 (/ s 60))))

 (define+ (random-grey)
   (var g (Math.round (_* 255 (Math.random))))
   (+ "rgb(" g "," g "," g
      ")"))

 (define+ (rgb r g b)
   (+ "rgb(" r "," g "," b
      ")"))

 (define+ (rgba r g b a)
   (+ "rgba(" r "," g "," b "," a
      ")"))

 (define+ (grey g)
   (rgb g g g))

 (define+ (r255)
   (Math.round (_* 255 (Math.random))))

 (define+ (random-rgb)
   (rgb (r255) (r255) (r255)))

 (define+ (random-int c w)
   (% 
    (Math.round (_+ (_- c (_/ w 2)) (_* w (Math.random))))
    255))

 (define+ (random-alpha c w)
   (var v (_+ (_- c (_/ w 2)) (_* w (Math.random))))
   (if (_> v 1) 1 (if (_< v 0) 0 v)))

 (define+ (make-color-source r wr g wg b wb a wa)
   (if (undefined? a) 
       (lambda ()
         (rgb (random-int r wr)
              (random-int g wg)
              (random-int b wb)))
     (lambda ()
       (rgba (random-int r wr)
             (random-int g wg)
             (random-int b wb)
             (random-alpha a wa)))))

 (define+ (make-color-easer startc endc transition)
   (var start undefined)
   (var-match [: rs gs bs] startc)
   (var-match [: re ge be] endc)
   (var wa (lambda (p a b)
             (_+ (_* a (_- 1 p))
                 (_* b p))))
   (lambda ()
     (if (undefined? start)
         (... (set! start (.. (new Date) (valueOf)))
              (rgb rs gs bs))
       (progn 
         (var now (.. (new Date) (valueOf)))
         (var d (_- now start))
         (var d% (_/ d transition))
         (if (_> d transition)
             (rgb re ge be)
           (rgb (Math.round (wa d% rs re))
                (Math.round (wa d% bs be))
                (Math.round (wa d% gs ge))))))))

 (define+ (make-color-easer-maker startc endc transition)
   (lambda ()
     (make-color-easer startc endc transition)))

 (define Ur-Clock-count 0)
 (define-class+ Ur-Clock Object
   (:* (as-options{} 
                  (title "Clock")
                  (container undefined)
                  (width 400)
                  (height 400)
                  (append-to (document.querySelector "body"))
                  (initial-transform undefined)))

   (if (undefined? container)
       (progn 
         (var container 
              (<>fragment 
               (<>div [:] 
                      (<>div [:class "title"]))))

         (set! this.container container)
         (.. append-to (appendChild container))))
   (var canvas (<>fragment 
                (<>canvas [: "class" "Clock-Ten"])))
   (var buttons (<>fragment
                 (<>div [:] 
                        (<>button [: "class" "Clock-Ten-toggle-dots"]
                                  "Toggle Dots")
                        (<>button [: "class" "Clock-Ten-toggle-hands"]
                                  "Toggle Hands")
                        (<>button [: "class" "Clock-Ten-save"]
                                  "Save"))))
   (.. this container (appendChild canvas))
   (.. this container (appendChild buttons))
   (set! canvas.width width)
   (set! canvas.height height)
   (set! this.canvas canvas)
   (set! this.context (.. canvas (getContext "2d")))
   (if (undefined? initial-transform)
       (... 
        (this.context.translate (/ width 2) (/ height 2))
        (this.context.scale 1 -1)
        (this.context.scale (/ height 1000) (/ height 1000))))
   (set! this.initial-transform 
         this.context.currentTransform)
   (var clock this)
   (set! this.ur-component (kran.component (lambda () (set! this.clock clock))))
   (set! this.kran-group (+ :Ur-Clock Ur-Clock-count))
   (set! Ur-Clock-count (+ 1 Ur-Clock-count))
   (set! this.groups {})
   (set! this.groups.all 
         (lambda ()
           ([kran.system clock.kran-group])))

   (set! this.components {})
   (set! this.c$ this.components)
   (set! this.systems {})
   (set! this.going false)
   (set! this.kran kran)
   this) 

 (define-method Ur-Clock component (name cons)
   (set! [this.components name] (kran.component cons)))

 (define (f-drop-first f)
   (_function ()
              (_return (f.apply this (Array.prototype.slice.call arguments 1 arguments.length)))))

 (define-method Ur-Clock localize-group-name (name)
   (if (undefined? name)
       this.kran-group
     (_+ this.kran-group 
         (.. (name.charAt 0) (toUpperCase))
         (name.slice 1))))

 (define-method Ur-Clock system (name sys)
   (var sub-group sys.group)
   (var sys* {})
   (var real-group-name undefined)
   (for (prop in sys)
        (match prop
               ("components" 
                (set! sys*.components 
                      (if (undefined? sys.components)
                          [: this.ur-component]
                        (prefix this.ur-component sys.components))))
               ("pre" (set! sys*.pre sys.pre))
               ("every"
                (set! sys*.every 
                      (f-drop-first sys.every)))
               ("post"
                (set! sys*.post 
                      (f-drop-first sys.post)))
               ("arrival"
                (set! sys*.arrival 
                      (f-drop-first sys.arrival)))
               (other (set! [sys* other] [sys other]))))
   (set! sys*.group (this.localize-group-name [sys "group"]))
   (if (defined? sys.group)
       (set! [this.groups sys.group]
             (lambda ()
               ([kran.system sys*.group]))))
   (set! [this.systems name] (kran.system sys*)))

 (define-method Ur-Clock getEntityCollection (components)
   (var collection (kran.getEntityCollection (prefix this.ur-component components)))
   (var old collection.forEachWithComps)
   (set! collection.forEachWithComps 
         (lambda (block)
           (old.call this 
                     (_function 
                      ()
                      (block.apply this (Array.prototype.slice.call arguments 1 arguments.length))))))
   collection)

 (define-method Ur-Clock on-entities (query block)
   (var collection (kran.getEntityCollection (prefix this.ur-component query)))
   (collection.forEachWithComps 
    (_function ()
               (_return (block.apply this (Array.prototype.slice.call arguments 1 arguments.length))))))

 (define-method Ur-Clock entity ()
   (var self this)
   (var e (kran.entity))
   (var old-add e.add)
   (set! e.add 
         (lambda (c :* rest)
           (if (string? c)
               (old-add.apply e (prefix [self.components c] rest))
             (old-add.apply e (prefix c rest)))))
   (e.add this.ur-component)
   e)

 (define-method Ur-Clock start ()
   (var self this)
   (if this.going undefined
     (progn
       (set! this.going true)
       (var loop 
            (_function 
             () 
             (self.groups.all)
             (if self.going 
                 (requestAnimationFrame loop))))
       (loop))))

 (define-method Ur-Clock stop ()
   (set! this.going false)))
paldepind commented 10 years ago

I've commited an update to Kran today that might fix your issue. Now one needs to instantiate Kran in order to use it. Every instance has its own set of components, systems and entities - it works just like I described before.

Please let me know what you think and if this is of any use for you.

VincentToups commented 10 years ago

That sounds like exactly the sort of thing I'd like - I'll use it in my next piece and let you know how it is. -V

On Sun, Oct 20, 2013 at 6:01 PM, Simon Friis Vindum < notifications@github.com> wrote:

I've commited an update to Kran today that might fix your issue. Now one needs to instantiate Kran in order to use it. Every instance has its own set of components, systems and entities - it works just like I described before.

Please let me know what you think and if this is of any use for you.

— Reply to this email directly or view it on GitHubhttps://github.com/paldepind/Kran/issues/1#issuecomment-26675806 .

paldepind commented 10 years ago

That sounds good! I'll close the issue for now - but in case anything comes up, let me know!