Programming Clojure笔记之九——snake游戏源码解析

(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 ~@(map import-field fields-to-do)
         ~@(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 75)
(def height 50)
(def point-size 10)
(def turn-millis 75)
(def win-length 5)
(def dirs { VK_LEFT [-1 0]
            VK_RIGHT [ 1 0]
            VK_UP [ 0 -1]
            VK_DOWN [ 0 1]})

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 0) (pt 1) 1 1]))

;返回代表苹果的map
(defn create-apple [] 
  {:location [(rand-int width) (rand-int height)]
   :color (Color. 210 50 90)
   :type :apple}) 

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

;传入蛇的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])) 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值