天天看點

Programming Clojure筆記之九——snake遊戲源碼解析The Functional ModelBuilding a Mutable Model with STMThe Snake GUI

(ns reader.snake
  (:import (java.awt Color Dimension)
           (javax.swing JPanel JFrame Timer JOptionPane)
           (java.awt.event ActionListener KeyListener))
  (:use examples.import-static))
(import-static java.awt.event.KeyEvent VK_LEFT VK_RIGHT VK_UP VK_DOWN)
           

首先使用ns聲明了名稱空間,然後導入了需要的Java類和另一個名稱空間,最後使用了名稱空間的import-static宏導入一些靜态成員。接下來看看import-static宏是如何編寫的。

(defmacro import-static
  [class & fields-and-methods]
  (let [only (set (map str fields-and-methods))
        the-class (. Class forName (str class))
        static? (fn [x]
                    (. java.lang.reflect.Modifier (isStatic (. x (getModifiers)))))
        statics (fn [array]
                    (set (map (memfn getName) (filter static? array))))
        all-fields (statics (. the-class (getFields)))
        all-methods (statics (. the-class (getMethods)))
        fields-to-do (intersection all-fields only)
        methods-to-do (intersection all-methods only)
        make-sym (fn [string]
                     (with-meta (symbol string) {:private true}))
        import-field (fn [name]
                         (list 'def (make-sym name) (list '. class (symbol name))))
        import-method (fn [name]
                          (list 'defmacro (make-sym name) '[& args] (list 'list ''. (list 'quote class) (list 'apply 'list (list 'quote (symbol name)) 'args))))]
    `(do [email protected](map import-field fields-to-do)
         [email protected](map import-method methods-to-do))))
           

為了閱讀友善,删除了檔案頭部的名稱空間聲明和宏文檔字元串。

首先看參數清單,class為傳入的Java類(如上文的java.awt.event.KeyEvent),fields-and-methods為要導入的靜态域和方法名。

接下來是一個很長的let,用來綁定了很多的局部名稱。

- only是一個傳入的靜态域和靜态方法的集合

- the-class是傳入類的Class對象,用于反射

- static?是一個函數,用于判定傳入的類成員是否為靜态成員。

- statics是一個函數,接收一個類成員的數組,傳回所有的靜态成員名。

- all-fields代表目前類的所有靜态域

- all-methods代表目前類的所有靜态方法。

- fields-to-do要導入的靜态域

- methods-to-do要導入的靜态方法

- make-sym是一個函數,根據傳入的字元串建立一個symbol

- import-field是一個函數,使用def建立靜态域對應的symbol

- import-method是一個函數,使用defmacro建立靜态域對應的宏(要導入的靜态方法被定義為宏)。

The Functional Model

(def width)
(def height)
(def point-size)
(def turn-millis)
(def win-length)
(def dirs { VK_LEFT [-1]
            VK_RIGHT []
            VK_UP [ -1]
            VK_DOWN []})
           

width表示視窗寬有75個正方形,height表示視窗高有50個正方形,point-size表示每個正方形的邊長(像素)。

turn-millis表示多長時間重新整理一次遊戲視窗(重繪),win-length表示赢得遊戲時snake的長度。dirs表示方向向量。

;用于兩個坐标向量相加,傳回結果向量
(defn add-points [& pts] 
  (vec (apply map + pts)))

;根據點的坐标向量計算該正方形的繪制資訊(包括左上角的坐标和寬高,以像素為機關)
(defn point-to-screen-rect [pt] 
  (map #(* point-size %) 
       [(pt) (pt)]))

;傳回代表蘋果的map
(defn create-apple [] 
  {:location [(rand-int width) (rand-int height)]
   :color (Color.)
   :type :apple}) 

;傳回代表蛇的map
(defn create-snake []
  {:body (list [1]) 
   :dir [1]
   :type :snake
   :color (Color.)})

;傳入蛇的map,首先蛇頭和方向向量相加,然後判斷grow參數,grow參數存在就加上原來全部的蛇身,否則就加上舍棄尾部的蛇身。
;注意函數中對傳入map的拆解并綁定相應的字段,:as後面的符号綁定整個map
(defn move [{:keys [body dir] :as snake} & grow]
  (assoc snake :body (cons (add-points (first body) dir) 
               (if grow body (butlast body)))))

;更改蛇map的:dir字段
(defn turn [snake newdir] 
  (assoc snake :dir newdir))

;判斷是否赢得了遊戲
(defn win? [{body :body}]
  (>= (count body) win-length))

;判斷除頭部的身體集合是否包含了蛇頭。
(defn head-overlaps-body? [{[head & body] :body}]
  (contains? (set body) head))

;判斷遊戲是否失敗
(def lose? head-overlaps-body?)

;判斷頭部和蘋果的坐标向量是否相等(是否吃掉了蘋果)
(defn eats? [{[snake-head] :body} {apple :location}]
   (= snake-head apple))
           

Building a Mutable Model with STM

注意下面函數的參數都是ref,因而必要的時候使用了解引用reader macro(@)跟上面的函數進行銜接。

;每次timer回調都會調用該函數,更改蛇的狀态,要麼吃掉蘋果增長一節,要麼正常移動。
(defn update-positions [snake apple]
  (dosync
   (if (eats? @snake @apple)
     (do (ref-set apple (create-apple))
         (alter snake move :grow))
     (alter snake move)))
  nil)

;更改snake的方向向量
(defn update-direction [snake newdir]
  (when newdir (dosync (alter snake turn newdir))))

;重置遊戲,即建立新的蛇和蘋果
(defn reset-game [snake apple]
  (dosync (ref-set apple (create-apple))
      (ref-set snake (create-snake)))
  nil)
           

The Snake GUI

;該函數繪制一個方塊
(defn fill-point [g pt color] 
  (let [[x y width height] (point-to-screen-rect pt)]
    (.setColor g color) 
    (.fillRect g x y width height)))

;該處多重方法,分别用來繪制蘋果和蛇,注意此處使用了匿名方法,根據第二個參數的:type字段進行排程
(defmulti paint (fn [g object & _] (:type object)))

(defmethod paint :apple [g {:keys [location color]}]
  (fill-point g location color))

;doseq作用為,将body中的每個點向量綁定到point上,并進行繪制  
(defmethod paint :snake [g {:keys [body color]}]
  (doseq [point body]
    (fill-point g point color)))

;函數傳回一個實作了ActionListener和KeyListener接口的JPanel對象
;proxy用于繼承一個類并且實作接口,在這兒是繼承了JPanel,然後實作了後面的接口。
(defn game-panel [frame snake apple]
  (proxy [JPanel ActionListener KeyListener] []
    (paintComponent [g]
      (proxy-super paintComponent g)
      (paint g @snake)
      (paint g @apple))
    (actionPerformed [e]
      (update-positions snake apple)
      (when (lose? @snake)
          (reset-game snake apple)
          (JOptionPane/showMessageDialog frame "You lose!"))
      (when (win? @snake)
          (reset-game snake apple)
          (JOptionPane/showMessageDialog frame "You win!"))
      (.repaint this))
    (keyPressed [e]
      (update-direction snake (dirs (.getKeyCode e))))
    (getPreferredSize [] 
      (Dimension. (* (inc width) point-size) 
          (* (inc height) point-size)))
    (keyReleased [e])
    (keyTyped [e])))

;遊戲入口函數,建立snake,apple,frame,panel,timer
(defn game [] 
  (let [snake (ref (create-snake))
    apple (ref (create-apple))
    frame (JFrame. "Snake")
    panel (game-panel frame snake apple)
    timer (Timer. turn-millis panel)]
    (doto panel
      (.setFocusable true)
      (.addKeyListener panel))
    (doto frame 
      (.add panel)
      (.pack)
      (.setVisible true))
    (.start timer)
    [snake, apple, timer])) 
           

繼續閱讀