October 3, 2024
By: Kevin

clojureDart使用Flame开发游戏

  1. 背景
  2. 🧑‍💻 咨询服务 🧑‍💻
  3. 🗞️ 新闻与链接: 应用发布, Re-Dash Inspector 和 HYTRADBOI 2025 🗞️
    1. 应用发布
    2. Re-Dash Inspector
    3. HYTRADBOI 2025(作者的广告)
  4. 🕹️ 在ClojureDart中实现Flame的打砖块游戏 🕹️
    1. 项目设置
    2. 步骤#4: 创建游戏
    3. 步骤#5: 显示球体
    4. 步骤#6: 实现反弹
    5. 步骤#7: 让球与球拍互动
    6. 步骤#8: 打破墙壁
    7. 跳过后续步骤
    8. 处理遗留问题
  5. 最后的思考
  6. 结语
    1. 参考内容

由于Dart是一门有运行时范型的语言, clojureDart最让我疑惑的地方在于如何处理类型的meta-info. 这篇文章给了我挺多启发.

全文翻译自Breakout Game in ClojureDart, 保留了作者的广告内容.

本文将介绍如何在ClojureDart中跟随Flame框架的打砖块教程(Brick Breaker Tutorial)开发一款打砖块游戏.

背景

Ian Chow(他最近刚在应用商店发布了一款CLJD应用)提到, 在移植这个教程时遇到了一些困难.

我们很重视这个问题, 积极动手实践! 本文将涉及大量代码, 并且由于我们对Flame框架不太熟悉, 代码可能不太符合Clojure的风格.

如果已经迫不及待, 可以直接跳到🕹️表情符号部分.

🧑‍💻 咨询服务 🧑‍💻

如果你正在考虑构建桌面, 移动甚至是网页应用, 请与我们联系. 我们将在多个层面为你提供帮助, 从技术支持和专业知识, 到与你一起构建应用.

🗞️ 新闻与链接: 应用发布, Re-Dash Inspector 和 HYTRADBOI 2025 🗞️

应用发布

恭喜Ian Chow在应用商店发布了一款新的ClojureDart应用🎉.

Re-Dash Inspector

Werner Kok开发的Re-Dash Inspector非常酷🤯, 原因有二:

  1. 它是Flutter开发工具(类似于浏览器开发工具)的扩展, 帮助处理Re-Dash应用(Re-Dash是"ClojureDart和Flutter的re-frame").
  2. 它是用ClojureDart编写的!

HYTRADBOI 2025(作者的广告)

这不仅仅是关于Clojure的内容......HYTRADBOI大会又回来了!

内容新颖, 价格便宜. 绝对不容错过!

更正式的Datalog 2.0 2024会议将于下周举行(也是Christophe的生日! ). 我们希望视频和论文能尽快公开!

🕹️ 在ClojureDart中实现Flame的打砖块游戏 🕹️

game

打开原始教程的单独标签页, 重点在于将Dart代码翻译为ClojureDart代码, 我们不会重复教程中的解释.

在每一步中, 新代码或修改后的代码由👇或👈指示.

由于本文将包含大量代码, 你可以随时跳到结论部分!

项目设置

首先, 创建项目目录并初始化clojureDart项目.

mkdir brickbreaker
cd brickbreaker
fvm use
clj -M:cljd init

然后添加所需的flutter依赖项(fvm是fluter的版本管理工具, 是可选的):

fvm flutter pub add flame flutter_animate google_fonts

创建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.))))

接着运行:

clj -M:cljd flutter -d macos ; 根据目标平台进行调整

完成步骤#3.

步骤#4: 创建游戏

首先, 我们介绍PlayArea, 目前使用deftype, 也许使用reify就足够了.

我暂时不确定: 这是边写边做的. 同样, mixin参数化了一个尚未定义的类BrickBreaker, 因此暂时省略. 也许我们甚至不需要它.

(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) ; 有一个引用, 但我还不知道接下来怎么做

(defn main []
  (f/run
    (game/GameWidget. game (game/FlameGame.))))

接下来是缺失的BrickBreaker类.

(这是相关部分的直接链接, 但由于某些JS限制, 无法在页面中间插入链接🙄)

(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)

; 👇
(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
    (^:getter width [this] (-> this .-size .-x))
    (^:getter height [this] (-> this .-size .-x)))

(defn main []
  (f/run
    (game/GameWidget. game (BrickBreaker.) #_👈)))

现在, 你应该能看到米色的矩形!

步骤#5: 显示球体

继续深入.

(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)

; 👇
(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)
    (.-position! this (.+ (.-position this) (.* velocity dt)))
    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.))))

球体显示

步骤#6: 实现反弹

继续跟随原始教程, 我们现在为球体添加与墙壁的碰撞检测.

(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)

(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
      (not (instance? PlayArea other)) (println "与" other "发生碰撞")
      ; 有意不简化/重构
      (<= (-> 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.))))

步骤#7: 让球与球拍互动

(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)

(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) 1)
                             (* (-> this .-game .-width) 0.3))))
      :else (println "与" 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 [{:keys [x y]} (.-size this)] (m/Size. x y))) ; .toSize由扩展提供
        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.0 (-> 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.))))

碰撞

步骤#8: 打破墙壁

(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)

(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) 1)
                             (* (-> 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 "与" 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 [{:keys [x y]} (.-size this)] (m/Size. x y)))
        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.0 (-> 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 [{:keys [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)))
      ; 👇
      ; 跳过await, 因为我不明白为什么要等待addAll而不是其他添加
      (.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.))))

最终

跳过后续步骤

教程的后续步骤更多涉及Flutter相关内容, 因此我们在此停止.

处理遗留问题

编译器报告了5个动态警告:

DYNAMIC WARNING: can't resolve member width on target type FlameGame of library package:flame/src/game/flame_game.dart  (no source location)
...

这些警告与我们未参数化的HasGameReference有关. 尝试将其更改为#/(HasGameReference BrickBreaker)时, 遇到了类型之间的循环问题. 可以通过先声明BrickBreaker类而不实现任何内容, 然后在最后再声明现有的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)

; 👇
; 预先声明BrickBreaker以修复循环问题
(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) 1)
                             (* (-> 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 "与" 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 [{:keys [x y]} (.-size this)] (m/Size. x y)))
        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.0 (-> 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 [{:keys [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)))
      ; 👇
      ; 跳过await, 因为我不明白为什么要等待addAll而不是其他添加
      (.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)))))
    ; 👈
    ; 移除调试覆盖层, 更加美观
    )
  ^: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.))))

最终效果

最后的思考

跟随这个教程让我们和编译器都走出了舒适区. 在这个过程中, 我们修复了三个互操作性的小错误(你知道getter/setter对可能在返回/期望类型上不一致吗? 你知道类的超类在应用mixin后可能不是extends后指定的类吗? ).

大约200行代码, 还不错.

游戏玩法一般, 但这只是一个教程, 没关系.

键盘输入的实现依赖于键盘重复, 因此球拍的移动不够平滑连续. 我没有找到内置的方法来每帧通知已按下的键. 因此, 你需要自己实现一个.

没有连续移动意味着无法精细控制球拍速度如何与球体速度在碰撞时结合.

上述代码是Dart版本的直接移植, 并不了解Flame框架. 通过引入更通用的组件, 可以使代码更符合Clojure风格. 然而, 这些组件在查询时依赖于类型缓存, 因此更通用的组件需要其他过滤方法.

此外, 代码中存在大量的变异和嵌套.

创建一个好的辅助器/包装器将是一个有趣的挑战.

一个好的第一步是使用更面向数据, 少依赖类型的方法重新实现这个打砖块游戏, 看看在哪些地方存在阻抗不匹配的问题.

结语

通过本文的步骤, 我们成功在ClojureDart中实现了一款基本的打砖块游戏. 虽然过程中遇到了一些挑战, 但通过不断尝试和调整, 我们解决了这些问题, 并对ClojureDart与Flame框架的结合有了更深入的理解. 希望本文对正在尝试类似项目的你有所帮助!

如果你在开发过程中遇到任何问题, 欢迎在下方留言讨论, 或者联系我方咨询服务, 我们将竭诚为你提供技术支持和专业指导.


以上为翻译全文.

参考内容

  1. 使用dart开发打砖块的链接
Tags: clojure flutter dart