面向对象游戏案例:贪吃蛇

案例相关源码以上传到 GitHub :https://github.com/lipengzhou/new-snake

案例介绍

游戏演示

在线演示地址:贪吃蛇

案例目标

游戏的目的是用来体会js高级语法的使用 不需要具备抽象对象的能力,使用面向对象的方式分析问题,需要一个漫长的过程。

功能实现

搭建页面

放一个容器盛放游戏场景 div#map,设置样式

  1. #map {
  2. width: 800px;
  3. height: 600px;
  4. background-color: #ccc;
  5. position: relative;
  6. }

分析对象

  • 游戏对象
  • 蛇对象
  • 食物对象

创建食物对象

  • Food

    • 属性

      • x
      • y
      • width
      • height
      • color
    • 方法

      • render 随机创建一个食物对象,并输出到map上
  • 创建Food的构造函数,并设置属性

  1. var position = 'absolute';
  2. var elements = [];
  3. function Food(x, y, width, height, color) {
  4. this.x = x || 0;
  5. this.y = y || 0;
  6. // 食物的宽度和高度(像素)
  7. this.width = width || 20;
  8. this.height = height || 20;
  9. // 食物的颜色
  10. this.color = color || 'green';
  11. }
  • 通过原型设置render方法,实现随机产生食物对象,并渲染到map上
  1. Food.prototype.render = function (map) {
  2. // 随机食物的位置,map.宽度/food.宽度,总共有多少分food的宽度,随机一下。然后再乘以food的宽度
  3. this.x = parseInt(Math.random() * map.offsetWidth / this.width) * this.width;
  4. this.y = parseInt(Math.random() * map.offsetHeight / this.height) * this.height;
  5. // 动态创建食物对应的div
  6. var div = document.createElement('div');
  7. map.appendChild(div);
  8. div.style.position = position;
  9. div.style.left = this.x + 'px';
  10. div.style.top = this.y + 'px';
  11. div.style.width = this.width + 'px';
  12. div.style.height = this.height + 'px';
  13. div.style.backgroundColor = this.color;
  14. elements.push(div);
  15. }
  • 通过自调用函数,进行封装,通过window暴露Food对象
  1. window.Food = Food;

创建蛇对象

  • Snake

  • 属性

    • width 蛇节的宽度 默认20
    • height 蛇节的高度 默认20
    • body 数组,蛇的头部和身体,第一个位置是蛇头
    • direction 蛇运动的方向 默认right 可以是 left top bottom
  • 方法

    • render 把蛇渲染到map上
  • Snake构造函数

  1. var position = 'absolute';
  2. var elements = [];
  3. function Snake(width, height, direction) {
  4. // 设置每一个蛇节的宽度
  5. this.width = width || 20;
  6. this.height = height || 20;
  7. // 蛇的每一部分, 第一部分是蛇头
  8. this.body = [
  9. {x: 3, y: 2, color: 'red'},
  10. {x: 2, y: 2, color: 'red'},
  11. {x: 1, y: 2, color: 'red'}
  12. ];
  13. this.direction = direction || 'right';
  14. }
  • render方法
  1. Snake.prototype.render = function(map) {
  2. for(var i = 0; i < this.body.length; i++) {
  3. var obj = this.body[i];
  4. var div = document.createElement('div');
  5. map.appendChild(div);
  6. div.style.left = obj.x * this.width + 'px';
  7. div.style.top = obj.y * this.height + 'px';
  8. div.style.position = position;
  9. div.style.backgroundColor = obj.color;
  10. div.style.width = this.width + 'px';
  11. div.style.height = this.height + 'px';
  12. }
  13. }
  • 在自调用函数中暴露Snake对象
  1. window.Snake = Snake;

创建游戏对象

游戏对象,用来管理游戏中的所有对象和开始游戏

  • Game

    • 属性

      • food

      • snake

      • map

    • 方法

      • start 开始游戏(绘制所有游戏对象)
  • 构造函数
  1. function Game(map) {
  2. this.food = new Food();
  3. this.snake = new Snake();
  4. this.map = map;
  5. }
  • 开始游戏,渲染食物对象和蛇对象
  1. Game.prototype.start = function () {
  2. this.food.render(this.map);
  3. this.snake.render(this.map);
  4. }

游戏的逻辑

写蛇的move方法

  • 在蛇对象(snake.js)中,在Snake的原型上新增move方法
  1. 让蛇移动起来,把蛇身体的每一部分往前移动一下
  2. 蛇头部分根据不同的方向决定 往哪里移动
  1. Snake.prototype.move = function (food, map) {
  2. // 让蛇身体的每一部分往前移动一下
  3. var i = this.body.length - 1;
  4. for(; i > 0; i--) {
  5. this.body[i].x = this.body[i - 1].x;
  6. this.body[i].y = this.body[i - 1].y;
  7. }
  8. // 根据移动的方向,决定蛇头如何处理
  9. switch(this.direction) {
  10. case 'left':
  11. this.body[0].x -= 1;
  12. break;
  13. case 'right':
  14. this.body[0].x += 1;
  15. break;
  16. case 'top':
  17. this.body[0].y -= 1;
  18. break;
  19. case 'bottom':
  20. this.body[0].y += 1;
  21. break;
  22. }
  23. }
  • 在game中测试
  1. this.snake.move(this.food, this.map);
  2. this.snake.render(this.map);

让蛇自己动起来

  • 私有方法

    1. 什么是私有方法?
    2. 不能被外部访问的方法
    3. 如何创建私有方法?
    4. 使用自调用函数包裹
  • 在game.js中 添加runSnake的私有方法,开启定时器调用蛇的move和render方法,让蛇动起来

  • 判断蛇是否撞墙
  1. function runSnake() {
  2. var timerId = setInterval(function() {
  3. this.snake.move(this.food, this.map);
  4. // 在渲染前,删除之前的蛇
  5. this.snake.render(this.map);
  6. // 判断蛇是否撞墙
  7. var maxX = this.map.offsetWidth / this.snake.width;
  8. var maxY = this.map.offsetHeight / this.snake.height;
  9. var headX = this.snake.body[0].x;
  10. var headY = this.snake.body[0].y;
  11. if (headX < 0 || headX >= maxX) {
  12. clearInterval(timerId);
  13. alert('Game Over');
  14. }
  15. if (headY < 0 || headY >= maxY) {
  16. clearInterval(timerId);
  17. alert('Game Over');
  18. }
  19. }.bind(that), 150);
  20. }
  • 在snake中添加删除蛇的私有方法,在render中调用
  1. function remove() {
  2. // 删除渲染的蛇
  3. var i = elements.length - 1;
  4. for(; i >= 0; i--) {
  5. // 删除页面上渲染的蛇
  6. elements[i].parentNode.removeChild(elements[i]);
  7. // 删除elements数组中的元素
  8. elements.splice(i, 1);
  9. }
  10. }
  • 在game中通过键盘控制蛇的移动方向
  1. function bindKey() {
  2. document.addEventListener('keydown', function(e) {
  3. switch (e.keyCode) {
  4. case 37:
  5. // left
  6. this.snake.direction = 'left';
  7. break;
  8. case 38:
  9. // top
  10. this.snake.direction = 'top';
  11. break;
  12. case 39:
  13. // right
  14. this.snake.direction = 'right';
  15. break;
  16. case 40:
  17. // bottom
  18. this.snake.direction = 'bottom';
  19. break;
  20. }
  21. }.bind(that), false);
  22. }
  • 在start方法中调用
  1. bindKey();

判断蛇是否吃到食物

  1. // 在Snake的move方法中
  2. // 在移动的过程中判断蛇是否吃到食物
  3. // 如果蛇头和食物的位置重合代表吃到食物
  4. // 食物的坐标是像素,蛇的坐标是几个宽度,进行转换
  5. var headX = this.body[0].x * this.width;
  6. var headY = this.body[0].y * this.height;
  7. if (headX === food.x && headY === food.y) {
  8. // 吃到食物,往蛇节的最后加一节
  9. var last = this.body[this.body.length - 1];
  10. this.body.push({
  11. x: last.x,
  12. y: last.y,
  13. color: last.color
  14. })
  15. // 把现在的食物对象删除,并重新随机渲染一个食物对象
  16. food.render(map);
  17. }

其它处理

把html中的js代码放到index.js中

避免html中出现js代码

自调用函数的参数

  1. (function (window, undefined) {
  2. var document = window.document;
  3. }(window, undefined))
  • 传入window对象

将来代码压缩的时候,可以吧 function (window) 压缩成 function (w)

  • 传入undefined

在将来会看到别人写的代码中会把undefined作为函数的参数(当前案例没有使用) 因为在有的老版本的浏览器中 undefined可以被重新赋值,防止undefined 被重新赋值

整理代码

现在的代码结构清晰,谁出问题就找到对应的js文件即可。 通过自调用函数,已经防止了变量命名污染的问题

但是,由于js文件数较多,需要在页面上引用,会产生文件依赖的问题(先引入那个js,再引入哪个js) 将来通过工具把js文件合并并压缩。现在手工合并js文件演示

  • 问题1
  1. // 如果存在多个自调用函数要用分号分割,否则语法错误
  2. // 下面代码会报错
  3. (function () {
  4. }())
  5. (function () {
  6. }())
  7. // 所以代码规范中会建议在自调用函数之前加上分号
  8. // 下面代码没有问题
  9. ;(function () {
  10. }())
  11. ;(function () {
  12. }())
  • 问题2
  1. // 当自调用函数 前面有函数声明时,会把自调用函数作为参数
  2. // 所以建议自调用函数前,加上;
  3. var a = function () {
  4. alert('11');
  5. }
  6. (function () {
  7. alert('22');
  8. }())