天天看点

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

继续阅读