Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

replace/refactor Gamestate and FSM modules with "Stateful Class" #48

Open
AlecTroemel opened this issue Nov 19, 2023 · 1 comment
Open

Comments

@AlecTroemel
Copy link
Owner

Last time I rewrote the FSM and gamestate modules I represented them as directed graphs. Im not a huge fan of how they ended up. Specifically I've grown to like modules to be "self contained". The ultimate goal being able to just copy/paste any one of the files if you'd like! Also the re-naming of macros is a bit goofy to me.

Since then I came across this random rust-lisp project, that had a really interesting class DSL that embedded states as a first class citizen of the class! In gamedev almost every class ends up needing states, so this api makes a lot of sense to me. It pretty cleanly represents the needs of FSM and gamestates!

Here's the half baked implementation I've used in a couple games

# Class Creator
# Inspired by https://gamelisp.rs/reference/state-machines.html
# dont worry though, it just wraps the common Table Prototype way of doing OOP in Janet

(defn first-as-keyword [[form-symbol & rest]]
  [(keyword form-symbol) ;rest])

(defn to-class-prototype [forms &opt state-name]
  (->> forms
       (map |(match (first-as-keyword $)
	       [:defn name & body]
	       (tuple (keyword name)
		      ~(fn ,(if state-name
			      (symbol state-name "/" name)
			      name)
			 ,;body))

	       [:def-state state-name & state-forms]
	       (tuple (keyword state-name)
		      (to-class-prototype state-forms state-name))

	       _ []))
       (from-pairs)))

(defn to-default-values [forms]
  (->> forms
       (map |(match (first-as-keyword $)
	       [:field name val]
	       (tuple name val)

	       [:def-state state-name & state-forms]
	       (tuple (keyword state-name)
		      (to-default-values state-forms))

	       _ []))
       (from-pairs)))

(defmacro def-class [name & forms]
  ~(upscope
     (def ,name ,(to-class-prototype forms))

     (defn ,(symbol name "/init") [& overrides]
       (default overrides [])
       (var tbl
	 (table/setproto
	   (merge ,(to-default-values forms)
		  (table ;overrides))
	   ,name))
       (when-let [init-fn (get tbl :init)]
	 (:init tbl))
       tbl)))

(defn GOTO [tab state-name & enter-args]
  (var old-state-name (tab :state))
  (put tab :state state-name)

  # Clear out old fields
  (each key (get tab :__state_keys__ [])
    (put tab key nil))

  # Copy new state keys out to tab
  (let [proto-tab (get (table/getproto tab) state-name)
	inst-tab (get tab state-name)
	merged-tab (merge @{} proto-tab inst-tab)]
    (put tab :__state_keys__ (keys merged-tab))
    (each [key val] (pairs merged-tab)
      (put tab key val)))

  # call the "on-enter" fn if present on new state
  (when-let [on-enter-fn (get tab :on-enter)]
    (on-enter-fn tab old-state-name ;enter-args))

  # return table for chaining
  tab)
@AlecTroemel
Copy link
Owner Author

example of how it can be used as a gamestate manager

(def-class Game
  # NOTE: you can put some shared state transition code here

  (def-state :title
    (defn on-enter [self & args]
      nil)

    (defn update [self dt]
      nil)

    (defn draw [self]
      (clear-screen :red)))


  (def-state :main
    (defn on-enter [self & args]
      nil)

    (defn update [self dt]
      nil)

    (defn draw [self]
      (clear-screen :blue))))

(def *GS* (-> (Game/init) (GOTO :main)))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant