前一阵子看算法,基础的数据结构有复习了一遍,最后还剩下一点经典的算法没有看,打算8月回头复习。近期看看JAVA、Web和Android。
今天看了一个JAVA游戏贪食蛇的例子,以前在linux上用c也写过一个类似的游戏,但是由于初窥门径,没有如此宏观的把握。以下是游戏构建的具体过程和思路。
一、架构
例子模式基于MVC模式,将数据模型、显示和控制分割开,使得整个程序模块清晰,耦合松散,最近研究研究STRUCTS框架,互为印证,颇有心得。
在编程之前,将这个问题调理搞清楚是很有意义的。对于贪食蛇游戏的直观感受是这样的,游戏者通过操作键盘的上下左右键来操纵蛇的前进方向,当蛇吃到食物时长度就加一,当蛇撞到墙或者撞到自己时,游戏就将停止。因此可以比较简单的提炼出三个组件:Snake、Food、Ground。这是游戏中的基本元素,接下来通过分析框架来提炼出其他元素。
上图是本示例的基本框架,其中Controller类负责处理按键事件和处理整个贪食蛇游戏的实例,而GamePanel类构成了贪食蛇游戏的前台,并且接收用户触发的按键事件。这是一个典型的MVC模式,其中Ground, Snake, Food构成了程序的组件模型,Controller构成了控制器,GamePanel构成了显示模块。
接下来就是讨论各个类的行为了。
首先是Snake,这是核心对象。Snake能干什么呢?Snake需要移动,所以有void move(),Snake需要在运动中改变方向,所以有void changeDirection(),Snake需要吃到食物,所以有void eatFood(),Snake需要判断是否吃到了自己,所以有boolean isEatBody(),Snake还需要在前台显示,所以有void drawMe(),最后Snake是一直沿着前进方向向前移动的,所以需要一个线程不停的调用move方法执行运行,我们将这个线程命名为SnakeDriver内部类,通过void start()方法来启动这个线程。基于以上的方法,我们可以清晰的看到Snake需要几个属性:direction(方向)、body(蛇身)。接下来要考虑使用的数据结构:direction用int型,毋庸置疑的;body用LinkedList<Point>类型,这是基于Snake的move方法的实现,在LinkedList的头部增加一个节点(根据当前运行方向)addFirst()方法,在LinkedList的尾部减少一个节点removeLast()方法。
然后是Ground,这个类比较简单,肯定需要有一个显示的方法void drawMe(),还需要判断Snake是否撞到了Ground上,所以有方法boolean isSnakeEatRock(),行为考虑完了就需要考虑数据结构,用简单的int型二维数组就可以了。
最后是Food,这个类也比较简单,因为Food类无非是显示前端的某个坐标上的一个正方形,因此继承Point类,显示方法void drawMe(),还需要判断Snake是否吃到了Food,因此方法boolean isSnakeEatFood()。
这三个类Snake, Ground, Food作为组件放在包.snake.entities中。
接下来分析前端的GamePanel类,放在包.snake.view中,GamePanel类作为前台的画布,我们选择继承JPanel类。由于其用于显示组件,到底是哪些组件呢?因此必有三个属性:snake, ground, food。采用一个void display()方法启动画布显示,并覆盖一个回调函数void paintComponent(Graphic g),在一个线程中不断刷新画布。
最后需要分析的控制核心Controller类,放在包.snake.controller中。前面已经说过,这个类完成两大功能,也即处理按键事件和处理整个贪食蛇游戏的事件。首先处理按键是简单的,通过继承KeyAdapter类并覆盖方法keyPressed()方法即可实现。事件监听应当也是一种固定的格式,首先定义一个事件监听接口SnakeListener类置于包.snake.listener包中,Controller类继承这个接口,并实现snakeMoved()方法。最后还需要控制游戏的开始,所以有方法void newGame()。
整个游戏的主进程在Game中,位于包.snake.game。
写到这里,已是一团雾水,为什么需要将前台的显示和核心的控制逻辑分离?比如监听键盘,在我看来为什么不是放在GamePanel中实现,而这些监听器偏偏是在程序初始化时在JFrame, JPanel, Snake中注册的,这些组件中都要addListener方法。