Compare commits

...

6 Commits

Author SHA1 Message Date
2bd7ad1a28 fix jitter kind of? 2026-07-31 22:17:41 -06:00
507faf8875 try to stop jitter 2026-07-30 22:25:10 -06:00
befcdfc352 More stuff 2026-07-27 22:01:08 -06:00
a179e453b1 stuff 2026-07-25 22:53:26 -06:00
ba10d04fa2 player rendering 2026-07-22 22:39:40 -06:00
f11ce94adf server side interp 2026-07-21 22:05:13 -06:00
8 changed files with 163 additions and 100 deletions

View File

@@ -4,8 +4,6 @@
["pixi.js" :as pixi] ["pixi.js" :as pixi]
[goog.dom :as dom] [goog.dom :as dom]
[cljs.core.async :as async :refer [go go-loop <! >!]] [cljs.core.async :as async :refer [go go-loop <! >!]]
[il.common.race :as race]
[il.common.class :as class]
[frontend.il.entity :as entity] [frontend.il.entity :as entity]
[frontend.il.update :as update])) [frontend.il.update :as update]))
@@ -25,7 +23,7 @@
right :a right :a
up :w up :w
down :s} (get-in @state [:player :keymap]) down :s} (get-in @state [:player :keymap])
speed (get-in @state [:player :speed] 5)] speed (get-in @state [:player :speed] 2.33)]
(swap! state update-in [:player :velocity] assoc (swap! state update-in [:player :velocity] assoc
:x (* speed (+ left right)) :x (* speed (+ left right))
:y (* speed (+ up down)))))) :y (* speed (+ up down))))))
@@ -38,12 +36,12 @@
(when (count outbox) (when (count outbox)
(go (go
(doseq [msg outbox] (doseq [msg outbox]
(println state) (js/console.log (clj->js state))
(println "Writing event: " msg) (js/console.log "Writing event: " (clj->js msg))
(>! tx-chan (js/JSON.stringify (clj->js msg)))))) (>! tx-chan (js/JSON.stringify (clj->js msg))))))
(assoc state :outbox [])) (assoc state :outbox []))
(defn ticker [app state layers rx-chan tx-chan t] (defn ticker [app state layers sb rx-chan tx-chan t]
(let [dt (.-deltaTime t) (let [dt (.-deltaTime t)
{world-layer :world-layer {world-layer :world-layer
entity-layer :entity-layer} layers entity-layer :entity-layer} layers
@@ -51,46 +49,47 @@
entity-relative-mouse-pos (.toLocal ^js entity-layer global-mouse-pos) entity-relative-mouse-pos (.toLocal ^js entity-layer global-mouse-pos)
send-interval 25 ; Send every 25ms (10 tps) send-interval 25 ; Send every 25ms (10 tps)
elapsed (.-elapsedMS t) elapsed (.-elapsedMS t)
{:keys [player entities]} (-> (update @state :elapsed + elapsed) {:keys [player entities players]} (-> (update @state :elapsed + elapsed)
(update/update-entities dt layers) (update/update-state :entities dt layers)
(update/update-player dt send-interval layers entity-relative-mouse-pos) (update/update-state :players dt layers)
(update/update-from-server rx-chan) (update/update-player dt send-interval layers entity-relative-mouse-pos sb)
(drain-outbox! tx-chan) (update/update-from-server rx-chan)
(->> (swap! state merge))) (drain-outbox! tx-chan)
(->> (swap! state merge)))
sx (- (/ (.. app -screen -width) 2) (player :x)) sx (- (/ (.. app -screen -width) 2) (player :x))
sy (- (/ (.. app -screen -height) 2) (player :y)) sy (- (/ (.. app -screen -height) 2) (player :y))
entity-renderables (->> (vals entities) entity-renderables (->> (vals entities)
(remove #(nil? (% :renderable))) (remove #(nil? (% :renderable)))
(vec)) (vec))
other-players-renderables (->> (vals players)
(remove #(nil? (% :renderable)))
(vec))
world-renderables (vector {:renderable world-layer :x sx :y sy}) world-renderables (vector {:renderable world-layer :x sx :y sy})
player-renderables (vector player player-renderables (vector player
(assoc player :renderable (:shootbox player))) (assoc player :renderable (:shootbox player)))
things-to-update (concat player-renderables entity-renderables world-renderables)] things-to-update (concat player-renderables
other-players-renderables
entity-renderables
world-renderables)]
(doseq [thing things-to-update] (doseq [thing things-to-update]
; TODO: Add lerp here, we shouldn't just launch things around with their X/Y
; instead if the difference between curr-pos and tar-pos is > than vel try to move it to their every frame via vel
(let [{renderable :renderable (let [{renderable :renderable
x :x x :x
y :y} thing] y :y} thing]
(set! (.. renderable -x) x) (when renderable
(set! (.. renderable -y) y))))) (js/console.log "Rendering thing: " x y)
(set! (.. renderable -x) x)
(set! (.. renderable -y) y))))))
(defn init [] (defn init []
(let [app (doto (pixi/Application.) (let [app (doto (pixi/Application.)
(.init {:resizeTo js/window :autoDensity true :resolution (.-devicePixelRatio js/window)})) (.init {:resizeTo js/window :autoDensity true :resolution (.-devicePixelRatio js/window)}))
state (atom {:player {:velocity {:x 0 :y 0} state (atom {:player {:velocity {:x 0 :y 0}
:keymap {:w 0 :a 0 :s 0 :d 0 :lmb 0} :keymap {:w 0 :a 0 :s 0 :d 0 :lmb 0}
:last-attack -1 :last-attack -1}
:attack-speed 0.66
:meta {:name "Player"
:race race/human
:class class/priest}
:sprite "https://pixijs.com/assets/bunny.png"
:speed 10
:renderable nil}
:elapsed 0 :elapsed 0
:outbox [] :outbox []
:entities {}}) :entities {}
:players {}})
width (.-width js/window.screen) width (.-width js/window.screen)
height (.-height js/window.screen) height (.-height js/window.screen)
stage (.-stage app) stage (.-stage app)
@@ -102,6 +101,11 @@
layers {:entity-layer entity-layer layers {:entity-layer entity-layer
:world-layer world-layer :world-layer world-layer
:ui-layer ui-layer} :ui-layer ui-layer}
sb-callback (fn [v _] (swap! state update-in [:player :keymap] assoc :lmb v))
sb (entity/player-shoot-box
entity-layer
(partial sb-callback true)
(partial sb-callback false))
websocket (doto (js/WebSocket. (str "ws://" js/location.host "/ws")) websocket (doto (js/WebSocket. (str "ws://" js/location.host "/ws"))
(.addEventListener "open" #(go-loop [] (.addEventListener "open" #(go-loop []
(when-let [msg (<! tx-chan)] (when-let [msg (<! tx-chan)]
@@ -121,15 +125,4 @@
(.addChild world-layer) ; Bottom of the layer stack (.addChild world-layer) ; Bottom of the layer stack
(.addChild entity-layer) ; Middle of the layer stack (.addChild entity-layer) ; Middle of the layer stack
(.addChild ui-layer)) ; Top of the layer stack (.addChild ui-layer)) ; Top of the layer stack
(let [e (merge {:x (-> width (/ 2) (int)) (.. app -ticker (add (partial ticker app state layers sb rx-chan tx-chan))))))))
:y (-> height (/ 2) (int))}
(@state :player))
p (entity/render entity-layer e)
sb-callback (fn [v _] (swap! state update-in [:player :keymap] assoc :lmb v))
sb (entity/player-shoot-box
entity-layer
(partial sb-callback true)
(partial sb-callback false))
player (assoc e :renderable p :shootbox sb)]
(swap! state update-in [:player] merge player)
(.. app -ticker (add (partial ticker app state layers rx-chan tx-chan)))))))))

View File

@@ -2,33 +2,41 @@
(:require (:require
[frontend.il.entity :as entity] [frontend.il.entity :as entity]
[cljs.core.async :as async] [cljs.core.async :as async]
[il.common.velocity :as v])) [il.common.math :as ilmath]))
(defn apply-velocity [e dt] (defn apply-velocity [e dt]
(if (contains? e :velocity) (if (contains? e :velocity)
(let [{x :x y :y vel :velocity} e (let [{x :x y :y vel :velocity} e
{vx :x vy :y} vel {vx :x vy :y} vel
dtvx (* dt vx) dtvx (Math/floor (* dt vx))
dtvy (* dt vy)] dtvy (Math/floor (* dt vy))]
(assoc e :x (+ x dtvx) :y (+ y dtvy))) (assoc e :x (+ x dtvx) :y (+ y dtvy)))
e)) e))
(defn apply-renderable [e layers] (defn apply-renderable [e layers]
(if-not (contains? e :renderable) (if (and (nil? (e :renderable)) (e :sprite))
(let [{entity-layer :entity-layer} layers (let [{entity-layer :entity-layer} layers
renderable (entity/render entity-layer e)] renderable (entity/render entity-layer e)]
(assoc e :renderable renderable)) (assoc e :renderable renderable))
e)) e))
(defn apply-server-position [e] (defn apply-server-position [e]
(let [{:keys [x y tx ty]} e (let [{:keys [x y tx ty]} e
speed (get e :speed 15) speed (get e :speed 15)
min-distance-for-update 1
has-server-pos (not (and (nil? tx) (nil? ty))) has-server-pos (not (and (nil? tx) (nil? ty)))
has-actual-pos (not (and (nil? x) (nil? y)))] has-actual-pos (not (and (nil? x) (nil? y)))
velocityless-entity (dissoc e :velocity)]
(cond (cond
(and has-server-pos (not has-actual-pos)) (assoc e :x tx :y ty) ; If the position given by the server is the same as the current position, do nothing
(and has-server-pos has-actual-pos) (assoc e :velocity (v/velocity-for x y tx ty speed)) (and has-server-pos (= x tx) (= y ty)) velocityless-entity
; When the entity has a rendered position, and a new server position
(and has-server-pos has-actual-pos) (let [delta (ilmath/euclidean-distance x y tx ty)]
(if (> delta min-distance-for-update)
(assoc velocityless-entity :velocity (ilmath/velocity-for x y tx ty speed))
velocityless-entity))
; When the entity has no rendered position, and a new server position
has-server-pos (assoc velocityless-entity :x tx :y ty)
:else e))) :else e)))
(defn apply-player-shoot [state mouse-pos] (defn apply-player-shoot [state mouse-pos]
@@ -36,11 +44,13 @@
(< (+ (get-in state [:player :last-attack] -1) (< (+ (get-in state [:player :last-attack] -1)
(* 1000 (get-in state [:player :attack-speed] 0.75))) (* 1000 (get-in state [:player :attack-speed] 0.75)))
(js/Date.now))) (js/Date.now)))
(-> state (let [{x :x y :y} (state :player)]
(update :outbox conj {:type "shoot" (-> state
:x (.-x mouse-pos) (update :outbox conj {:type "shoot"
:y (.-y mouse-pos)}) :player {:x x :y y}
(assoc-in [:player :last-attack] (js/Date.now))) :x (.-x mouse-pos)
:y (.-y mouse-pos)})
(assoc-in [:player :last-attack] (js/Date.now))))
state)) state))
(defn apply-player-position [state send-interval] (defn apply-player-position [state send-interval]
@@ -54,23 +64,26 @@
(update :outbox conj {:type "position" :x x :y y})) (update :outbox conj {:type "position" :x x :y y}))
state))) state)))
;; TODO: Cleanup entities somehow (defn update-state [state key dt layers]
(defn update-entities [state dt layers] (let [{v key} state
(let [{entities :entities} state _ (js/console.log (clj->js v))
new-entities (update-vals entities #(-> % new-values (update-vals v #(-> %
(apply-server-position) (apply-server-position)
(apply-renderable layers) (apply-renderable layers)
(apply-velocity dt)))] (apply-velocity dt)))]
(assoc state :entities new-entities))) (assoc state key new-values)))
(defn update-player [state dt send-interval layers mouse-pos] (defn update-player [state dt send-interval layers mouse-pos sb]
(let [{player :player} state (let [{player :player} state
updated-player (-> player updated-player (-> player
(apply-renderable layers) (apply-renderable layers)
(assoc :shootbox sb)
(apply-velocity dt))] (apply-velocity dt))]
(-> (assoc state :player updated-player) (if-not (and (nil? player) (nil? (player :renderable)))
(apply-player-position send-interval) (-> (assoc state :player updated-player)
(apply-player-shoot mouse-pos)))) (apply-player-position send-interval)
(apply-player-shoot mouse-pos))
(update state :outbox conj {:type "request-init"}))))
(defn- deep-merge [& maps] (defn- deep-merge [& maps]
(apply merge-with (apply merge-with
@@ -83,14 +96,27 @@
(defn update-from-server [state rx-chan] (defn update-from-server [state rx-chan]
(loop [state-acc state (loop [state-acc state
{:keys [type] :as msg} (async/poll! rx-chan)] {:keys [type] :as msg} (async/poll! rx-chan)]
(if (and (not (nil? msg)) (= type "sync")) (if-not (nil? msg)
(let [entities (msg :entities) (cond
state-entities (state-acc :entities) (= type "init") (update state-acc :player merge (dissoc msg :type))
tx-entities (update-vals entities (fn [e] {:sprite (:sprite e) (= type "sync") (let [entities (msg :entities)
:tx (:x e) players (if-not (nil? (get-in state-acc [:player :id]))
:ty (:y e)})) (dissoc (msg :players) (get-in state-acc [:player :id]))
merged-entities (deep-merge state-entities tx-entities)] {})
(recur state-entities (state-acc :entities)
(assoc state-acc :entities merged-entities) state-players (state-acc :players)
(async/poll! rx-chan))) tx-entities (update-vals entities (fn [e] {:sprite (:sprite e)
:tx (:x e)
:ty (:y e)}))
tx-players (update-vals players (fn [e] {:sprite (:sprite e)
:tx (:x e)
:ty (:y e)}))
merged-entities (deep-merge state-entities tx-entities)
merged-players (deep-merge state-players tx-players)]
(recur
(assoc state-acc
:entities merged-entities
:players merged-players)
(async/poll! rx-chan)))
:else state-acc)
state-acc))) state-acc)))

View File

@@ -0,0 +1 @@
(ns il.common.constant)

View File

@@ -1,4 +1,4 @@
(ns il.common.velocity) (ns il.common.math)
(defn velocity-for [x y tx ty speed] (defn velocity-for [x y tx ty speed]
(let [dx (- tx x) (let [dx (- tx x)
@@ -9,3 +9,6 @@
(if (zero? h) (if (zero? h)
{:x 0.0 :y 0.0} {:x 0.0 :y 0.0}
{:x vx :y vy}))) {:x vx :y vy})))
(defn euclidean-distance [x1 y1 x2 y2]
(Math/sqrt (+ (Math/pow (- x2 x1) 2) (Math/pow (- y2 y1) 2))))

View File

@@ -1 +0,0 @@
(ns il.constant)

View File

@@ -5,33 +5,41 @@
[compojure.route :as route] [compojure.route :as route]
[clojure.core.async :as async :refer [<! >! go-loop]] [clojure.core.async :as async :refer [<! >! go-loop]]
[hiccup.core :as h] [hiccup.core :as h]
[il.player :as p]
[il.state :as s] [il.state :as s]
[il.json :as j] [il.json :as j]
[il.handler :as handler] [il.handler :as handler]
[il.system.movement :as movement])) [il.system.movement :as movement]))
(defn ws-handler [state state-chan player-id] (defn ws-handler [state state-chan player-id]
(println "Starting session for " player-id)
(go-websocket [in out err] (go-websocket [in out err]
(s/change state-chan update :players assoc player-id {:out out :in in :err err :x 0 :y 0})
(go-loop [] (go-loop []
(when-let [e (<! err)] (when-let [e (<! err)]
(println (.getMessage e)) (println (.getMessage e))
(recur))) (recur)))
(go-loop [] (go-loop []
(if-let [raw (<! in)] (if-not (contains? (@state :players) player-id)
(when (contains? (@state :players) player-id) ; Only if player has been initialized do we accept messages (do (s/change state-chan update :players assoc player-id {:out out :in in :err err
:x 0 :y 0
:initialized false
:data (merge p/default-player {:id player-id})})
(recur))
(if-let [raw (<! in)]
(let [msg (j/from-json raw) (let [msg (j/from-json raw)
type (get msg :type "invalid")] type (get msg :type "invalid")
state @state]
(cond (cond
(= type "position") (handler/handle-position @state state-chan player-id msg) (= type "position") (handler/handle-position state state-chan player-id msg)
(= type "shoot") (handler/handle-shoot @state state-chan player-id msg) (= type "shoot") (handler/handle-shoot state state-chan player-id msg)
(= type "invalid") (>! out "Hello World!")) (= type "request-init") (handler/handle-request-init state state-chan player-id msg)
(= type "invalid") (>! out "Invalid command!"))
(println msg) (println msg)
(println @state)) (println state)
(recur)) (recur))
(do (do
(s/change state-chan update :players dissoc player-id) (s/change state-chan update :players dissoc player-id)
(println "Closed: " player-id)))))) (println "Closed: " player-id)))))))
(defn index-handler [_] (defn index-handler [_]
(h/html (h/html
@@ -53,7 +61,7 @@
(defn game-loop [state state-chan] (defn game-loop [state state-chan]
(go-loop [] (go-loop []
(let [start-time (System/currentTimeMillis) (let [start-time (System/currentTimeMillis)
max-inputs-per-tick 15000 ; Stops the game from spinning for too long with events endlessly, dropping ticks max-inputs-per-tick 15000 ; Stops the game from spinning for too long with events endlessly dropping ticks
events-to-apply (loop [inputs [] events-to-apply (loop [inputs []
count 0] count 0]
(if (>= count max-inputs-per-tick) (if (>= count max-inputs-per-tick)
@@ -68,15 +76,32 @@
system-state (reduce (fn [state system] (system state)) system-state (reduce (fn [state system] (system state))
event-state event-state
(event-state :systems)) (event-state :systems))
entities (:entities @state) updated-state (swap! state conj system-state)
players (:players @state)] entities (updated-state :entities)
(swap! state conj system-state) players (updated-state :players)]
(doseq [{out :out} (vals players)] (doseq [{:keys [initialized data out] :as player} (vals players)]
(>! out (j/to-json {:type "sync" (let [player-id (data :id)]
:entities entities})))) (println player)
(when-not (nil? out)
(when-not initialized
(let [coords (select-keys player [:x :y])
data-to-send (conj data coords)
msg (assoc data-to-send :type "init")]
(println "Initializing " player-id)
(async/put! out (j/to-json msg))
(s/change state-chan update-in [:players player-id] assoc :initialized true)))
(let [players (update-vals players (fn [e] (-> e
(select-keys [:x :y :data])
(#(merge (dissoc % :data) (get % :data))))))
players-without-player-id (dissoc players player-id)] ; Strip the selected player from the update
(async/put! out (j/to-json {:type "sync"
:entities entities
:players players-without-player-id})))))))
(let [elapsed (- (System/currentTimeMillis) start-time) (let [elapsed (- (System/currentTimeMillis) start-time)
sleep-time (max 0 (- 50 elapsed))] sleep-time (max 0 (- 50 elapsed))]
(<! (async/timeout sleep-time))) (<! (async/timeout sleep-time)))
(recur)))) (recur))))
(defn -main [] (defn -main []

View File

@@ -1,20 +1,24 @@
(ns il.handler (ns il.handler
(:require [il.state :as s] (:require [il.state :as s]
[il.common.velocity :as v])) [il.common.math :as ilmath]))
(defn handle-position [_ state-chan player-id {x :x y :y}] (defn handle-position [_ state-chan player-id {x :x y :y}]
(s/change state-chan update-in [:players player-id] assoc :x x :y y)) (s/change state-chan update-in [:players player-id] assoc :x x :y y))
(defn handle-shoot [state state-chan player-id msg] (defn handle-request-init [_ state-chan player-id _]
(s/change state-chan update-in [:players player-id] assoc :initialized false))
(defn handle-shoot [_ state-chan player-id msg]
(let [bullet-id (random-uuid) (let [bullet-id (random-uuid)
player (get-in state [:players player-id]) ; player (get-in state [:players player-id])
{tx :x ty :y player :player} msg
{x :x y :y} player {x :x y :y} player
{tx :x ty :y} msg speed 50 ; TODO: Get projectile speed from player weapon
speed 25 ; TODO: Get projectile speed from player weapon velocity (ilmath/velocity-for x y tx ty speed)
velocity (v/velocity-for x y tx ty speed)
bullet {:sprite "/sprite/projectile.png" ; TODO: Render sprite based on player weapon bullet {:sprite "/sprite/projectile.png" ; TODO: Render sprite based on player weapon
:x x :x x
:y y :y y
:speed speed :speed speed
:player-id player-id
:velocity velocity}] :velocity velocity}]
(s/change state-chan update :entities assoc bullet-id bullet))) (s/change state-chan update :entities assoc bullet-id bullet)))

12
src/il/player.clj Normal file
View File

@@ -0,0 +1,12 @@
(ns il.player
(:require
[il.common.race :as race]
[il.common.class :as class]))
(def default-player
{:attack-speed 0.66
:meta {:name "Player"
:race race/human
:class class/priest}
:sprite "https://pixijs.com/assets/bunny.png"
:speed 10})