From d78b02ff88b3669362648f0ad8c131188e770430 Mon Sep 17 00:00:00 2001 From: Stuart Campbell Date: Mon, 1 Apr 2013 13:41:34 +1100 Subject: [PATCH] Initial thoughts on component-entity system --- common/src/clj/enoki/component_macros.clj | 15 ++++++++ common/src/cljx/enoki/entity.cljx | 25 +++++++++++++ example/common/src/cljx/game/component.cljx | 14 +++++++ example/common/src/cljx/game/main.cljx | 41 ++++++++++++++------- 4 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 common/src/clj/enoki/component_macros.clj create mode 100644 common/src/cljx/enoki/entity.cljx create mode 100644 example/common/src/cljx/game/component.cljx diff --git a/common/src/clj/enoki/component_macros.clj b/common/src/clj/enoki/component_macros.clj new file mode 100644 index 0000000..f359147 --- /dev/null +++ b/common/src/clj/enoki/component_macros.clj @@ -0,0 +1,15 @@ +(ns enoki.component-macros) + +(defmacro defcomponent + "Define a component-creation function, named by `type`, that accepts `params` + and returns a map consisting of the remaining key-value pairs. E.g. + + (defcomponent foo [bar baz] + :bar bar + :baz (inc baz)) + + (foo :quux 2) ; => {:enoki.component/type :foo, :bar :quux, :baz 3}" + [type params & kvs] + `(defn ~type ~(vec params) + (into {:enoki.component/type ~(keyword type)} + (hash-map ~@kvs)))) diff --git a/common/src/cljx/enoki/entity.cljx b/common/src/cljx/enoki/entity.cljx new file mode 100644 index 0000000..c864753 --- /dev/null +++ b/common/src/cljx/enoki/entity.cljx @@ -0,0 +1,25 @@ +;; Functions for initialising and managing entities. +;; Entities are plain mappings of component types (keywords) to components (which +;; are also maps). + +(ns enoki.entity) + +(let [next-id (atom 0)] + (defn uid + "Reserve and return a unique numeric ID." + [] + (swap! next-id inc))) + +(defn new + "Initialise an entity with some collection of components. A unique ID is + assigned and accessible via `:id`. Components are accessible by their + name as a keyword; i.e. use `:foo` to fetch components defined using + `(defcomponent foo ...)`." + [& components] + (into {:id (uid)} + (map (fn [comp] [(:enoki.component/type comp) comp]) components))) + +(defn with-component + "Find all entities that include a component of a given type." + [entities component-type] + (filter #(contains? % component-type) entities)) diff --git a/example/common/src/cljx/game/component.cljx b/example/common/src/cljx/game/component.cljx new file mode 100644 index 0000000..17d1f65 --- /dev/null +++ b/example/common/src/cljx/game/component.cljx @@ -0,0 +1,14 @@ +;; Game-specific components + +^:clj (ns game.component + (:use [enoki.component-macros :only [defcomponent]])) + +^:cljs (ns game.component + (:use-macros [enoki.component-macros :only [defcomponent]])) + +(defcomponent position [x y] + :x x + :y y) + +(defcomponent sprite [image-id] + :image-id image-id) diff --git a/example/common/src/cljx/game/main.cljx b/example/common/src/cljx/game/main.cljx index 7b7f458..36fb002 100644 --- a/example/common/src/cljx/game/main.cljx +++ b/example/common/src/cljx/game/main.cljx @@ -2,27 +2,32 @@ (:require [clojure.string :as str] [enoki.asset :as asset] [enoki.engine :as enoki] + [enoki.entity :as entity] [enoki.event :as event] [enoki.graphics :as gfx] [enoki.logging] - [enoki.logging-macros :as log]) + [enoki.logging-macros :as log] + [game.component :as comp]) (:use [enoki.core :only [now]])) ^:cljs (ns game.main (:require [clojure.string :as str] [enoki.asset :as asset] [enoki.engine :as enoki] + [enoki.entity :as entity] [enoki.event :as event] [enoki.graphics :as gfx] - [enoki.logging :as _]) + [enoki.logging :as _] + [game.component :as comp]) (:require-macros [enoki.logging-macros :as log]) (:use [enoki.core :only [now]]) (:use-macros [enoki.cljs-macros :only [double]])) -;; ## Update - (defn initial-state [] - {:alien {:position {:x 10 :y 60}}}) + {:entities [(entity/new (comp/sprite "images/alien.png") + (comp/position 10 60))]}) + +;; ## Update (defn movement [pressed-keys] (let [x-offsets {:left -1 :right 1} @@ -39,25 +44,35 @@ ;; ## Draw -(defn print-fps [ctx fps] +(defn image [state id] + (get-in state [:assets id])) + +(defn print-fps! [ctx fps] (gfx/draw-text! ctx (format "%03.1ffps" (double fps)) 10 20)) -(defn print-pressed-keys [ctx keys] +(defn print-pressed-keys! [ctx keys] (gfx/draw-text! ctx (format "keys: %s" (str/join ", " keys)) 10 40)) +(defn draw-sprite! [ctx state entity] + (let [image-id (get-in entity [:sprite :image-id]) + {:keys [x y]} (get entity :position)] + (gfx/draw-image! ctx (image state image-id) x y))) + +(defn draw-sprites! [ctx state] + (doseq [e (entity/with-component (:entities state) :sprite)] + (draw-sprite! ctx state e))) + (defn render [state ctx] (-> ctx (gfx/clear!) - (print-fps (enoki/fps state)) - (print-pressed-keys (:pressed-keys state)) - (gfx/draw-image! (get-in state [:assets "images/alien.png"]) - (get-in state [:alien :x]) - (get-in state [:alien :y])))) + (print-fps! (enoki/fps state)) + (print-pressed-keys! (:pressed-keys state)) + (draw-sprites! state))) ;; ## Loop (defn enter-loop [env] - (event/subscribe! :update (fn [state _] (update state))) + ;(event/subscribe! :update (fn [state _] (update state))) (event/subscribe! :render (fn [state _ ctx] (render state ctx))) (enoki/start (assoc env :state ;; FIXME: stupid hack to get assets into :state