This issue is about following the Flame’s Brick Breaker tutorial in ClojureDart.
This was prompted by Ian Chow (who just released a CLJD app onto the stores) mentioning, en passant, he struggled to port this tutorial.
We couldn’t let this slip, so here we go! This is going to be code-heavy, and not very clojurey as we were not familiar with the Flame framework.
If you are impatient, skip to the 🕹️ emojis.
If you are looking into building a desktop, mobile or even web app, get in touch with us we will help you at multiple levels from technical support and expertise to building it for you and with you.
Kudos to Ian Chow who released a new ClojureDart app to the stores 🎉.
The re-dash inspector
by Werner Kok is very very cool 🤯 for two reasons:
It's not about Clojure but... HYTRADBOI conf is back!!!
"Have You Tried Rubbing A DB On It?" (HYTRADBOI) was a really great conf (go watch the videos!) and it's happening anew in February 2025. There will be a Programming Languages track too!
It's fresh, cheap, online and async. You can't afford to miss it!
More formal but Datalog 2.0 2024 will be held next week (and on Christophe's birthday!). We hope videos and papers surface soon!
Open the original tutorial in a separate tab as the focus is on translating Dart to ClojureDart, we won't repeat the tutorial explanations.
At each step, new or modified code is pointed to by 👇 or 👈.
This is going to be code-heavy, so feel free to jump to the end for our conclusions!
Let's create the project directory and add Flutter to the path.
mkdir brickbreaker
cd brickbreaker
fvm use
clj -M:cljd init
Then add required dependencies:
fvm flutter pub add flame flutter_animate google_fonts
Create src/brickbreaker/main.cljd
:
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
[cljd.flutter :as f]))
(defn main []
(f/run
(game/GameWidget .game (game/FlameGame))))
and then
clj -M:cljd flutter -d macos ; adjust to your target
Et voilà we are caught up with step #3
First we introduce the PlayArea
, I use a deftype
for now, maybe a reify
would suffice. I don't know: I'm writing this as I go. In the same vein the mixin is parametrized by a yet undefined class (BrickBreaker
) so I omit it for now. Maybe we won't even need it.
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["dart:async" :as async] ; 👈
["package:flame/components.dart" :as components] ; 👈
[cljd.flutter :as f]))
(def game-width 820.0) ; 👈
(def game-height 1600.0) ; 👈
; 👇
(deftype PlayArea []
:extends (components/RectangleComponent .paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf))))
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 game-width game-height)))
^:mixin components/HasGameReference) ; there's a ref but I don't know yet where we are going
(defn main []
(f/run
(game/GameWidget .game (game/FlameGame))))
Next the missing BrickBreaker class
(it's a direct link to the relevant section but then some JS kicks in and decide that you can't link in the middle of a page 🙄)
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["dart:async" :as async]
["package:flame/components.dart" :as components]
[cljd.flutter :as f]))
(def game-width 820.0)
(def game-height 1600.0)
(deftype PlayArea []
:extends (components/RectangleComponent .paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf))))
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 game-width game-height)))
^:mixin components/HasGameReference) ; there's a ref but I don't know yet where we are going
; 👇
(deftype BrickBreaker []
:extends (game/FlameGame
.camera (components/CameraComponent.withFixedResolution
.width game-width
.height game-height))
(onLoad [this]
(.onLoad ^super this)
(-> this .-camera .-viewfinder (.-anchor! components/Anchor.topLeft))
(-> this .-world (.add (PlayArea))))
BrickBreaker ; 👇 I have a bad feeling about these getters
(^:getter width [this] (-> this .-size .-x))
(^:getter height [this] (-> this .-size .-x)))
(defn main []
(f/run
(game/GameWidget .game (BrickBreaker) #_👈)))
Behold the beige rectangle!
Are things getting serious yet?
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["dart:async" :as async]
["package:flame/components.dart" :as components]
[cljd.flutter :as f]))
(def game-width 820.0)
(def game-height 1600.0)
(def ball-radius (* game-width 0.02))
(deftype PlayArea []
:extends (components/RectangleComponent .paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf))))
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 game-width game-height)))
^:mixin components/HasGameReference) ; there's a ref but I don't know yet where we are going
; 👇
(deftype Ball [^game/Vector2 velocity]
:extends (components/CircleComponent
.anchor components/Anchor.center
.paint (doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! m/PaintingStyle.fill)))
(update [this dt]
(.update ^super this dt)
; 👇 .+ and .* because we call the native + and * operators which are not restricted to numbers
(.-position! this (.+ (.-position this) (.* velocity dt)))
nil))
; 👇 creating a constructor function to pass position radius
; maybe we should allow the ^super type hint onto fields in deftype
(defn ball [velocity position radius]
(doto (Ball velocity)
(.-position! position)
(.-radius! radius)))
(deftype BrickBreaker []
:extends (game/FlameGame
.camera (components/CameraComponent.withFixedResolution
.width game-width
.height game-height))
(onLoad [this]
(.onLoad ^super this)
(-> this .-camera .-viewfinder (.-anchor! components/Anchor.topLeft))
(doto (.-world this)
(.add (PlayArea))
; 👇
(.add (ball
(game/Vector2 (* (- (rand) 0.5) (.-width this)) (* 0.2 (.-height this)))
; 👇 calling the native / operator but ./ is not a valid symbol so we have to desugar into this form
(. (.-size this) / 2)
ball-radius)))
(.-debugMode! this true)) ; 👈
BrickBreaker
(^:getter width [this] (-> this .-size .-x))
(^:getter height [this] (-> this .-size .-x)))
(defn main []
(f/run
(game/GameWidget .game (BrickBreaker))))
Still following the original tutorial we now add collision with the walls.
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["dart:async" :as async]
["package:flame/collisions.dart" :as collisions] ; 👈
["package:flame/components.dart" :as components]
[cljd.flutter :as f]))
(def game-width 820.0)
(def game-height 1600.0)
(def ball-radius (* game-width 0.02))
(deftype PlayArea []
:extends (components/RectangleComponent
.paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf)))
.children [(collisions/RectangleHitbox)]) ; 👈
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 game-width game-height)))
^:mixin components/HasGameReference) ; there's a ref but I don't know yet where we are going
(deftype Ball [^game/Vector2 velocity]
:extends (components/CircleComponent
.anchor components/Anchor.center
.paint (doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! m/PaintingStyle.fill))
.children [(collisions/CircleHitbox)]) ; 👈
(update [this dt]
(.update ^super this dt)
(.-position! this (.+ (.-position this) (.* velocity dt)))
nil)
^:mixin components/HasGameReference ; 👈 same as before I omit the type parameter for now
^:mixin collisions/CollisionCallbacks ; 👈
; 👇
(onCollisionStart [this intersection-points other]
(.onCollisionStart ^super this intersection-points other)
(cond
(not (instance? PlayArea other)) (println "collision with" other)
; deliberaely not trying to simplify/refactor this
(<= (-> intersection-points .-first .-x) 0) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-width) (-> intersection-points .-first .-x)) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-height) (-> intersection-points .-first .-y)) (.removeFromParent this))
nil))
(defn ball [velocity position radius]
(doto (Ball velocity)
(.-position! position)
(.-radius! radius)))
(deftype BrickBreaker []
:extends (game/FlameGame
.camera (components/CameraComponent.withFixedResolution
.width game-width
.height game-height))
(onLoad [this]
(.onLoad ^super this)
(-> this .-camera .-viewfinder (.-anchor! components/Anchor.topLeft))
(doto (.-world this)
(.add (PlayArea))
(.add (ball
(game/Vector2 (* (- (rand) 0.5) (.-width this)) (* 0.2 (.-height this)))
(. (.-size this) / 2)
ball-radius)))
(.-debugMode! this true))
^:mixin game/HasCollisionDetection ; 👈
BrickBreaker
(^:getter width [this] (-> this .-size .-x))
(^:getter height [this] (-> this .-size .-x)))
(defn main []
(f/run
(game/GameWidget .game (BrickBreaker))))
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["package:flutter/services.dart" :as services] ; 👈
["dart:async" :as async]
["package:flame/collisions.dart" :as collisions]
["package:flame/components.dart" :as components]
["package:flame/effects.dart" :as effects] ; 👈
["package:flame/events.dart" :as events] ; 👈
[cljd.flutter :as f]))
(def game-width 820.0)
(def game-height 1600.0)
(def ball-radius (* game-width 0.02))
(def bat-width (* game-width 0.2)) ; 👈
(def bat-height (* ball-radius 2)) ; 👈
(def bat-step (* game-width 0.05)) ; 👈
(deftype PlayArea []
:extends (components/RectangleComponent
.paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf)))
.children [(collisions/RectangleHitbox)])
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 game-width game-height)))
^:mixin components/HasGameReference) ; there's a ref but I don't know yet where we are going
(deftype Ball [^game/Vector2 velocity]
:extends (components/CircleComponent
.anchor components/Anchor.center
.paint (doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! m/PaintingStyle.fill))
.children [(collisions/CircleHitbox)])
(update [this dt]
(.update ^super this dt)
(.-position! this (.+ (.-position this) (.* velocity dt)))
nil)
^:mixin components/HasGameReference
^:mixin collisions/CollisionCallbacks
(onCollisionStart [this intersection-points other]
(.onCollisionStart ^super this intersection-points other)
(cond
(instance? PlayArea other)
(cond
(<= (-> intersection-points .-first .-y) 0) (.-y! velocity (- (.-y velocity)))
(<= (-> intersection-points .-first .-x) 0) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-width) (-> intersection-points .-first .-x)) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-height) (-> intersection-points .-first .-y))
; 👇
(.add this (effects/RemoveEffect .delay 0.35)))
(instance? Bat other) ; 👈
; 👇
(do
(.-y! velocity (- (.-y velocity)))
(.-x! velocity (+ (.-x velocity)
(* (- (-> this .-position .-x) (-> other .-position .-x))
(-> other .-size .-x /)
(-> this .-game .-width)
0.3))))
:else (println "collision with" other))
nil))
(defn ball [velocity position radius]
(doto (Ball velocity)
(.-position! position)
(.-radius! radius)))
; 👇
(deftype Bat [corner-radius]
:extends (components/PositionComponent
.anchor components/Anchor.center
.children [(collisions/RectangleHitbox)])
(render [this canvas]
(.render ^super this canvas)
(.drawRRect canvas
(m/RRect.fromRectAndRadius
(.& m/Offset.zero (let [{:flds [x y]} (.-size this)] (m/Size x y))) ; .toSize is provided by an extension
corner-radius)
(doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! (m/PaintingStyle.fill)))))
Bat
(moveBy [this dx]
(.add this
(effects/MoveToEffect
(game/Vector2
(.clamp (+ (-> this .-position .-x) dx) 0 (-> this .-game .-width))
(-> this .-position .-y))
(effects/EffectController .duration 0.1))))
^:mixin events/DragCallbacks
(onDragUpdate [this event]
(.onDragUpdate ^super this event)
(.-x! (.-position this)
(.clamp (+ (-> this .-position .-x) (-> event .-localDelta .-x)) 0 ^double (-> this .-game .-width)))
nil)
^:mixin components/HasGameReference)
; 👇
(defn bat [corner-radius position size]
(doto (Bat corner-radius)
(.-position! position)
(.-size! size)))
(deftype BrickBreaker []
:extends (game/FlameGame
.camera (components/CameraComponent.withFixedResolution
.width game-width
.height game-height))
(onLoad [this]
(.onLoad ^super this)
(-> this .-camera .-viewfinder (.-anchor! components/Anchor.topLeft))
(doto (.-world this)
(.add (PlayArea))
(.add (ball
(game/Vector2 (* (- (rand) 0.5) (.-width this)) (* 0.2 (.-height this)))
(. (.-size this) / 2)
ball-radius))
; 👇
(.add (bat
(m/Radius.circular (/ ball-radius 2))
(game/Vector2 (/ (.-width this) 2) (* (.-height this) 0.95))
(game/Vector2 bat-width bat-height))))
(.-debugMode! this true))
^:mixin game/HasCollisionDetection
^:mixin events/KeyboardEvents ; 👈
; 👇
(onKeyEvent [this event keys-pressed]
(.onKeyEvent ^super this event keys-pressed)
(condp = (.-logicalKey event)
services/LogicalKeyboardKey.arrowLeft
(-> this .-world .-children (#/(.query Bat) #_🙄) .-first (.moveBy (- bat-step)) )
services/LogicalKeyboardKey.arrowRight
(-> this .-world .-children (#/(.query Bat) #_🙄) .-first (.moveBy bat-step) ))
m/KeyEventResult.handled)
BrickBreaker
(^:getter width [this] (-> this .-size .-x))
(^:getter height [this] (-> this .-size .-y))) ; 🐞
(defn main []
(f/run
(game/GameWidget .game (BrickBreaker))))
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["package:flutter/services.dart" :as services]
["dart:async" :as async]
["package:flame/collisions.dart" :as collisions]
["package:flame/components.dart" :as components]
["package:flame/effects.dart" :as effects]
["package:flame/events.dart" :as events]
[cljd.flutter :as f]))
(def game-width 820.0)
(def game-height 1600.0)
(def ball-radius (* game-width 0.02))
(def bat-width (* game-width 0.2))
(def bat-height (* ball-radius 2))
(def bat-step (* game-width 0.05))
; 👇
(def brick-colors [(m/Color 0xfff94144),
(m/Color 0xfff3722c),
(m/Color 0xfff8961e),
(m/Color 0xfff9844a),
(m/Color 0xfff9c74f),
(m/Color 0xff90be6d),
(m/Color 0xff43aa8b),
(m/Color 0xff4d908e),
(m/Color 0xff277da1),
(m/Color 0xff577590)])
(def brick-gutter (* game-width 0.015)) ; 👈
(def brick-width (/ (- game-width (* brick-gutter (inc (count brick-colors)))) ; 👈
(count brick-colors))) ; 👈
(def brick-height (* game-height 0.03)) ; 👈
(def difficulty-modifier 1.03) ; 👈
(deftype PlayArea []
:extends (components/RectangleComponent
.paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf)))
.children [(collisions/RectangleHitbox)])
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 game-width game-height)))
^:mixin components/HasGameReference) ; there's a ref but I don't know yet where we are going
(deftype Ball [^game/Vector2 velocity difficulty-modifier #_👈]
:extends (components/CircleComponent
.anchor components/Anchor.center
.paint (doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! m/PaintingStyle.fill))
.children [(collisions/CircleHitbox)])
(update [this dt]
(.update ^super this dt)
(.-position! this (.+ (.-position this) (.* velocity dt)))
nil)
^:mixin components/HasGameReference
^:mixin collisions/CollisionCallbacks
(onCollisionStart [this intersection-points other]
(.onCollisionStart ^super this intersection-points other)
(cond
(instance? PlayArea other)
(cond
(<= (-> intersection-points .-first .-y) 0) (.-y! velocity (- (.-y velocity)))
(<= (-> intersection-points .-first .-x) 0) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-width) (-> intersection-points .-first .-x)) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-height) (-> intersection-points .-first .-y))
(.add this (effects/RemoveEffect .delay 0.35)))
(instance? Bat other)
(do
(.-y! velocity (- (.-y velocity)))
(.-x! velocity (+ (.-x velocity)
(* (- (-> this .-position .-x) (-> other .-position .-x))
(-> other .-size .-x /)
(-> this .-game .-width)
0.3))))
(instance? Brick other) ; 👈
; 👇
(do
(cond
(< (-> this .-position .-y) (- (-> other .-position .-y) (/ (-> other .-size .-y) 2)))
(.-y! velocity (- (.-y velocity)))
(> (-> this .-position .-y) (+ (-> other .-position .-y) (/ (-> other .-size .-y) 2)))
(.-y! velocity (- (.-y velocity)))
(< (-> this .-position .-x) (-> other .-position .-x))
(.-x! velocity (- (.-x velocity)))
(> (-> this .-position .-x) (-> other .-position .-x))
(.-x! velocity (- (.-x velocity))))
(.setFrom velocity (.* velocity difficulty-modifier)))
:else (println "collision with" other))
nil))
(defn ball [velocity position radius difficulty-modifier]
(doto (Ball velocity difficulty-modifier) ; 👈
(.-position! position)
(.-radius! radius)))
(deftype Bat [corner-radius]
:extends (components/PositionComponent
.anchor components/Anchor.center
.children [(collisions/RectangleHitbox)])
(render [this canvas]
(.render ^super this canvas)
(.drawRRect canvas
(m/RRect.fromRectAndRadius
(.& m/Offset.zero (let [{:flds [x y]} (.-size this)] (m/Size x y))) ; .toSize is provided by an extension
corner-radius)
(doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! (m/PaintingStyle.fill)))))
Bat
(moveBy [this dx]
(.add this
(effects/MoveToEffect
(game/Vector2
(.clamp (+ (-> this .-position .-x) dx) 0 (-> this .-game .-width))
(-> this .-position .-y))
(effects/EffectController .duration 0.1))))
^:mixin events/DragCallbacks
(onDragUpdate [this event]
(.onDragUpdate ^super this event)
(.-x! (.-position this)
(.clamp (+ (-> this .-position .-x) (-> event .-localDelta .-x)) 0 ^double (-> this .-game .-width)))
nil)
^:mixin components/HasGameReference)
(defn bat [corner-radius position size]
(doto (Bat corner-radius)
(.-position! position)
(.-size! size)))
; 👇
(deftype Brick []
:extends (components/RectangleComponent
.anchor components/Anchor.center
.children [(collisions/RectangleHitbox)])
^:mixin collisions/CollisionCallbacks
(onCollisionStart [this intersection-points other]
(.onCollisionStart ^super this intersection-points other)
(.removeFromParent this)
(let [{:flds [children] :as world} (-> this .-game .-world)]
(when (= 1 (count (#/(.query Brick) children)))
(doto world
(.removeAll (#/(.query Ball) children))
(.removeAll (#/(.query Ball) children)))))
nil)
^:mixin components/HasGameReference)
; 👇
(defn brick [position color]
(doto (Brick)
(.-position! position)
(.-size! (game/Vector2 brick-width brick-height))
(.-paint! (doto (m/Paint)
(.-color! color)
(.-style! m/PaintingStyle.fill)))))
(deftype BrickBreaker []
:extends (game/FlameGame
.camera (components/CameraComponent.withFixedResolution
.width game-width
.height game-height))
(onLoad [this]
(.onLoad ^super this)
(-> this .-camera .-viewfinder (.-anchor! components/Anchor.topLeft))
(doto (.-world this)
(.add (PlayArea))
(.add (ball
(game/Vector2 (* (- (rand) 0.5) (.-width this)) (* 0.2 (.-height this)))
(. (.-size this) / 2)
ball-radius
difficulty-modifier #_👈))
(.add (bat
(m/Radius.circular (/ ball-radius 2))
(game/Vector2 (/ (.-width this) 2) (* (.-height this) 0.95))
(game/Vector2 bat-width bat-height)))
; 👇
; skipping await as I don't see why we should await addAll and not all other adds
(.addAll (for [i (range (count brick-colors))
j (range 5)]
(brick
(game/Vector2
(+ (* (+ 0.5 i) brick-width) (* (inc i) brick-gutter))
(+ (* (+ j 3.0) brick-height) (* j brick-gutter)))
(nth brick-colors i)))))
(.-debugMode! this true))
^:mixin game/HasCollisionDetection
^:mixin events/KeyboardEvents
(onKeyEvent [this event keys-pressed]
(.onKeyEvent ^super this event keys-pressed)
(condp = (.-logicalKey event)
services/LogicalKeyboardKey.arrowLeft
(-> this .-world .-children (#/(.query Bat) #_🙄) .-first (.moveBy (- bat-step)) )
services/LogicalKeyboardKey.arrowRight
(-> this .-world .-children (#/(.query Bat) #_🙄) .-first (.moveBy bat-step) ))
m/KeyEventResult.handled)
BrickBreaker
(^:getter width [this] (-> this .-size .-x))
(^:getter height [this] (-> this .-size .-y)))
(defn main []
(f/run
(game/GameWidget .game (BrickBreaker))))
Next steps of the tutorial are more Flutter-related so I'm stopping there.
The compiler complains about 5 dynamic warnings:
DYNAMIC WARNING: can't resolve member width on target type FlameGame of library package:flame/src/game/flame_game.dart (no source location)
DYNAMIC WARNING: can't resolve member height on target type FlameGame of library package:flame/src/game/flame_game.dart (no source location)
DYNAMIC WARNING: can't resolve member width on target type FlameGame of library package:flame/src/game/flame_game.dart at line: 71, column: 9, file: brickbreaker/main.cljd
DYNAMIC WARNING: can't resolve member width on target type FlameGame of library package:flame/src/game/flame_game.dart at line: 115, column: 11, file: brickbreaker/main.cljd
DYNAMIC WARNING: can't resolve member width on target type FlameGame of library package:flame/src/game/flame_game.dart at line: 122, column: 7, file: brickbreaker/main.cljd
Sure enough they are tied to the HasGameReference
we left unparametrized. Trying to change it to #/(HasGameReference BrickBreaker)
, I hit a circularity issue between types. It can be fixed by declaring BrickBreaker
twice: a first time without any implementation and a second time (the existing one) at the end.
(ns brickbreaker.main
(:require
["package:flame/game.dart" :as game]
["package:flutter/material.dart" :as m]
["package:flutter/services.dart" :as services]
["dart:async" :as async]
["package:flame/collisions.dart" :as collisions]
["package:flame/components.dart" :as components]
["package:flame/effects.dart" :as effects]
["package:flame/events.dart" :as events]
[cljd.flutter :as f]))
(def game-width 820.0)
(def game-height 1600.0)
(def ball-radius (* game-width 0.02))
(def bat-width (* game-width 0.2))
(def bat-height (* ball-radius 2))
(def bat-step (* game-width 0.05))
(def brick-colors [(m/Color 0xfff94144),
(m/Color 0xfff3722c),
(m/Color 0xfff8961e),
(m/Color 0xfff9844a),
(m/Color 0xfff9c74f),
(m/Color 0xff90be6d),
(m/Color 0xff43aa8b),
(m/Color 0xff4d908e),
(m/Color 0xff277da1),
(m/Color 0xff577590)])
(def brick-gutter (* game-width 0.015))
(def brick-width (/ (- game-width (* brick-gutter (inc (count brick-colors))))
(count brick-colors)))
(def brick-height (* game-height 0.03))
(def difficulty-modifier 1.03)
; 👇
; pre-declaration of BrickBreaker to fix the circularity problem
(deftype BrickBreaker []
:extends game/FlameGame
^:mixin game/HasCollisionDetection
^:mixin events/KeyboardEvents
BrickBreaker
(^:getter width [this] 42)
(^:getter height [this] 42))
(deftype PlayArea []
:extends (components/RectangleComponent
.paint (doto (m/Paint) (.-color! (m/Color 0xfff2e8cf)))
.children [(collisions/RectangleHitbox)])
(onLoad [this]
(.onLoad ^super this)
(.-size! this (game/Vector2 (-> this .-game .-width) (-> this .-game .-height)))) ; 👈
^:mixin #/(components/HasGameReference BrickBreaker)) ; 👈
(deftype Ball [^game/Vector2 velocity difficulty-modifier]
:extends (components/CircleComponent
.anchor components/Anchor.center
.paint (doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! m/PaintingStyle.fill))
.children [(collisions/CircleHitbox)])
(update [this dt]
(.update ^super this dt)
(.-position! this (.+ (.-position this) (.* velocity dt)))
nil)
^:mixin #/(components/HasGameReference BrickBreaker)
^:mixin collisions/CollisionCallbacks
(onCollisionStart [this intersection-points other]
(.onCollisionStart ^super this intersection-points other)
(cond
(instance? PlayArea other)
(cond
(<= (-> intersection-points .-first .-y) 0) (.-y! velocity (- (.-y velocity)))
(<= (-> intersection-points .-first .-x) 0) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-width) (-> intersection-points .-first .-x)) (.-x! velocity (- (.-x velocity)))
(<= (-> this .-game .-height) (-> intersection-points .-first .-y))
(.add this (effects/RemoveEffect .delay 0.35)))
(instance? Bat other)
(do
(.-y! velocity (- (.-y velocity)))
(.-x! velocity (+ (.-x velocity)
(* (- (-> this .-position .-x) (-> other .-position .-x))
(-> other .-size .-x /)
(-> this .-game .-width)
0.3))))
(instance? Brick other)
(do
(cond
(< (-> this .-position .-y) (- (-> other .-position .-y) (/ (-> other .-size .-y) 2)))
(.-y! velocity (- (.-y velocity)))
(> (-> this .-position .-y) (+ (-> other .-position .-y) (/ (-> other .-size .-y) 2)))
(.-y! velocity (- (.-y velocity)))
(< (-> this .-position .-x) (-> other .-position .-x))
(.-x! velocity (- (.-x velocity)))
(> (-> this .-position .-x) (-> other .-position .-x))
(.-x! velocity (- (.-x velocity))))
(.setFrom velocity (.* velocity difficulty-modifier)))
:else (println "collision with" other))
nil))
(defn ball [velocity position radius difficulty-modifier]
(doto (Ball velocity difficulty-modifier) ; 👈
(.-position! position)
(.-radius! radius)))
(deftype Bat [corner-radius]
:extends (components/PositionComponent
.anchor components/Anchor.center
.children [(collisions/RectangleHitbox)])
(render [this canvas]
(.render ^super this canvas)
(.drawRRect canvas
(m/RRect.fromRectAndRadius
(.& m/Offset.zero (let [{:flds [x y]} (.-size this)] (m/Size x y))) ; .toSize is provided by an extension
corner-radius)
(doto (m/Paint)
(.-color! (m/Color 0xff1e6091))
(.-style! (m/PaintingStyle.fill)))))
Bat
(moveBy [this dx]
(.add this
(effects/MoveToEffect
(game/Vector2
(.clamp (+ (-> this .-position .-x) dx) 0 (-> this .-game .-width))
(-> this .-position .-y))
(effects/EffectController .duration 0.1))))
^:mixin events/DragCallbacks
(onDragUpdate [this event]
(.onDragUpdate ^super this event)
(.-x! (.-position this)
(.clamp (+ (-> this .-position .-x) (-> event .-localDelta .-x)) 0 ^double (-> this .-game .-width)))
nil)
^:mixin #/(components/HasGameReference BrickBreaker)) ; 👈
(defn bat [corner-radius position size]
(doto (Bat corner-radius)
(.-position! position)
(.-size! size)))
(deftype Brick []
:extends (components/RectangleComponent
.anchor components/Anchor.center
.children [(collisions/RectangleHitbox)])
^:mixin collisions/CollisionCallbacks
(onCollisionStart [this intersection-points other]
(.onCollisionStart ^super this intersection-points other)
(.removeFromParent this)
(let [{:flds [children] :as world} (-> this .-game .-world)]
(when (= 1 (count (#/(.query Brick) children)))
(doto world
(.removeAll (#/(.query Ball) children))
(.removeAll (#/(.query Ball) children)))))
nil)
^:mixin #/(components/HasGameReference BrickBreaker)) ; 👈
(defn brick [position color]
(doto (Brick)
(.-position! position)
(.-size! (game/Vector2 brick-width brick-height))
(.-paint! (doto (m/Paint)
(.-color! color)
(.-style! m/PaintingStyle.fill)))))
(deftype BrickBreaker []
:extends (game/FlameGame
.camera (components/CameraComponent.withFixedResolution
.width game-width
.height game-height))
(onLoad [this]
(.onLoad ^super this)
(-> this .-camera .-viewfinder (.-anchor! components/Anchor.topLeft))
(doto (.-world this)
(.add (PlayArea))
(.add (ball
(game/Vector2 (* (- (rand) 0.5) (.-width this)) (* 0.2 (.-height this)))
(. (.-size this) / 2)
ball-radius
difficulty-modifier))
(.add (bat
(m/Radius.circular (/ ball-radius 2))
(game/Vector2 (/ (.-width this) 2) (* (.-height this) 0.95))
(game/Vector2 bat-width bat-height)))
; skipping await as I don't see why we should await addAll and not all other adds
(.addAll (for [i (range (count brick-colors))
j (range 5)]
(brick
(game/Vector2
(+ (* (+ 0.5 i) brick-width) (* (inc i) brick-gutter))
(+ (* (+ j 3.0) brick-height) (* j brick-gutter)))
(nth brick-colors i)))))
#_ (.-debugMode! this true)) ; 👈
^:mixin game/HasCollisionDetection
^:mixin events/KeyboardEvents
(onKeyEvent [this event keys-pressed]
(.onKeyEvent ^super this event keys-pressed)
(condp = (.-logicalKey event)
services/LogicalKeyboardKey.arrowLeft
(-> this .-world .-children (#/(.query Bat) #_🙄) .-first (.moveBy (- bat-step)) )
services/LogicalKeyboardKey.arrowRight
(-> this .-world .-children (#/(.query Bat) #_🙄) .-first (.moveBy bat-step) ))
m/KeyEventResult.handled)
BrickBreaker
(^:getter width [this] (-> this .-size .-x))
(^:getter height [this] (-> this .-size .-y)))
(defn main []
(f/run
(game/GameWidget .game (BrickBreaker))))
Oh, I also removed the debug overlay, much nicer:
Following this tutorial brought us and the compiler out of our comfort zone. We fixed three fine interop bugs along the way (did you know that a getter/setter pair may disagree on the returned/expected type? Did you know that the super class of a class is not the class specified after extends
when mixins are applied?).
At about 200 lines of code it's not bad.
The gameplay is meh but it's ok it's a tutorial I guess.
The code above is a straight port of the Dart version without knowing anything about Flame. Things could be made more Clojure friendly by introducing more generic components. However they are dependencies on types for example in the way query
uses a cache indexed by types. So more generic components would call for other filtering methods.
There also is mutation and nesting all over the place.
Making a good helper/wrapper would be an interesting challenge.
A good first step would be to reimplement this breakout game with a more data-oriented and less type-centric approach and see where the impedance mismatches occur.