2048项目总结

做一个2048项目的总结
一个小小的2048游戏真的其实挺复杂的(新手上路)
首先是对静态页面来进行设计,确定整体的页面构造
然后先做出各个数字块静态的模板,设计好圆角,数字显示,大小,颜色
也就是先做出样式来,然后我们再将他们进行隐藏
后面根据需求来对他们进行显示

每一个方块都具有相同的大小,圆角等等
因此将其抽象成对象Tile
每个的位置都有行和列两个属性,因此使用绝对定位来进行布局

//设置整个的布局为绝对定位
.tile-container {
  position: absolute;
  left: 0;
  top: 0;

将所有的方块都进行设计渲染出来
在这里插入图片描述
接着把游戏成功或者失败时弹框做出来

<div class="mask status">
        <div class="content">Game Over!</div>
        <button>Try again</button>
      </div>

然后将其display设计为none,先隐藏
然后准备开始引入js来实现动态效果

1.对方块Tile这个对象应该进行一个初始化设计
每个方块是有行,列以及值属性组成:

function Tile(position, value) {
    this.row = position.row;
    this.column = position.column;
    this.value = value;
}

2.对整个Grid容器进行设计,即对整个方块们活动的地方进行一个设置


function Grid(size = 4) {
  this.size = size;
  this.cells = [];
  this.init();
}
//给Grid init方法,进行初始化
//初始化成为一个4*4的数组
Grid.prototype.init = function(size) {
  for (let row = 0; row < size; row++) {
    this.cells.push([]);
    for (let column = 0; column < size; column++) {
      this.cells[row].push(null);
    }
  }
};

再给其加一个add方法,以便于将方块添加进数组里面

Grid.prototype.add = function(tile) {
    this.cells[tile.row][tile.column] = tile;
};

完成后,准备进行渲染功能的实现
渲染是指将这个方块在网页上显示出来

function Render() {
this.tileContainer = document.querySelector('.tile-container');
}

// 渲染整个grid
Render.prototype.render = function(grid) {
  for (let row = 0; row < grid.size; row++) {
    for (let column = 0; column < grid.size; column++) {
      // 如果grid中某个cell不为空,则渲染这个cell
      if (grid.cells[row][column]) {
        this.renderTile(grid.cells[row][column]);
      }
    }
  }
};

// 渲染单个tile
Render.prototype.renderTile = function(tile) {
  // 创建一个tile-inner
  const tileInner = document.createElement('div');
  tileInner.setAttribute('class', 'tile-inner');
  tileInner.innerHTML = tile.value;

  // 创建一个tile
  const tileDom = document.createElement('div');
  let classList = [
    'tile',
    `tile-${tile.value}`,
    `tile-position-${tile.row + 1}-${tile.column + 1}`
  ];
  tileDom.setAttribute('class', classList.join(' '));
  tileDom.appendChild(tileInner);
  this.tileContainer.appendChild(tileDom);
};

渲染单个tile的思路是:首先调用DOM操作,定位在容器那里
然后创建div,设置其类名,并且显示数值为tile的值
这就是为什么之前就要把2到2048在前面都设计出来
这样后面渲染时,设置的类名对了,直接使用前面的样式

随机出现方块的逻辑实现
游戏开始时,方块出现是随机的,并且2和4的概率不同
那么思路应该是:获取Grid所有空闲的方格,然后利用随机数获取其中一个
因此要给Grid添加方法:

// 获取所有可用方格的位置
Grid.prototype.availableCells = function() {
//设置一个Cell用于记录空闲
  const availableCells = [];
  //对cell进行遍历
  for (let row = 0; row < this.cells.length; row++) {
    for (let column = 0; column < this.cells[row].length; column++) {
      // 如果当前方格没有内容,则其可用(空闲)
      if (!this.cells[row][column]) {
      //记录该点位的位置
        availableCells.push({ row, column });
      }
    }
  }
  return availableCells;
};

随机某个可用方格

// 随机获取某个可用方格的位置
Grid.prototype.randomAvailableCell = function() {
  // 调用之前设计的方法,获取到所有的空闲方格
  const cells = this.availableCells();
  //如果可用的大于0
  if (cells.length > 0) {
    // 利用Math.random()随机获取其中的某一个
    return cells[Math.floor(Math.random() * cells.length)];
  }
};

利用随机空闲的位置创建节点

let grid = new Grid();
let render = new Render();
for (let i = 0; i < 2; i++) {
  // 90%概率为2,10%为4
  const value = Math.random() < 0.9 ? 2 : 4;
  // 随机一个方格的位置
  const position = grid.randomAvailableCell();
  // 添加到grid中
  grid.add(new Tile(position, value));
}
//调用函数进行渲染
render.render(grid);

在做项目的时候,一定要对代码进行分割,不同的代码放到不同的包里
不同功能的函数进行分开,各自完成自己相应的功能
可能这就是架构思维吧,我是菜鸡,慢慢学

接下来要进行键盘监听,以方便对输入的操作进行处理
键盘的键位都对应着一个keyCode
可以查阅,也可以设计这个代码:

window.addEventListener('keyup', function(e) {
  console.log(e.keyCode);
});

然后分别按下方向键,来获取对应的keycode
38 => 上
37 => 左
39 => 下
40 => 右
然后要进行监听回调
增加一个监听器,用于监听键盘事件

//listener.js
function Listener({ move: moveFn }) {
  window.addEventListener('keyup', function(e) {
      switch (e.keyCode) {
      case 38:
        moveFn('向左');
        break;
      case 37:
        moveFn('向上');
        break;
      case 39:
        moveFn('向下');
        break;
      case 40:
        moveFn('向右');
        break;
    }
  });
}

this.listener = new Listener({
  move: function(direction) {
    console.log(direction);
  }
});

将监听到的存储于moveFn回调,传回到listener

方向向量化
这些方向,必须转化为向量,计算机才能读懂
因此我们要将这些键盘的动作进行向量化
上下左右的移动对应其实是行,列的值的变化

function Listener({ move: moveFn }) {
  window.addEventListener('keyup', function(e) {
    switch (e.keyCode) {
      case 38:
        moveFn({ row: -1, column: 0 });
        break;
      case 37:
        moveFn({ row: 0, column: -1 });
        break;
      case 39:
        moveFn({ row: 0, column: 1 });
        break;
      case 40:
        moveFn({ row: 1, column: 0 });
        break;
    }
  });
}

移动
移动个人感觉是最负责的地方,尽量详细分析,大家一起学习吧
移动应该遵循什么样的规则?
1.同一排,同一列的方块应该向同一个方向进行移动
2.每个方块都是移动到最后一个空白的位置
为了让方格移动,肯定是要对每一个方格都进行遍历的
那么遍历肯定是有先后顺序
正常是左上到右下顺序

Manager.prototype.getPaths = function(direction) {
  let rowPath = [];
  let columnPath = [];
  for (let i = 0; i < this.size; i++) {
    rowPath.push(i);
    columnPath.push(i);
  }
  return {
    rowPath,
    columnPath
  };
};

当方向向右的时候,遍历顺序应该是从右往左
当方向向下的时候,遍历顺序应该是从下往上
优化一下:

Manager.prototype.getPaths = function(direction) {
  let rowPath = [];
  let columnPath = [];
  for (let i = 0; i < this.size; i++) {
    rowPath.push(i);
    columnPath.push(i);
  }

  // 向右的时候
  if (direction.column === 1) {
    columnPath = columnPath.reverse();
  }

  // 向下的时候
  if (direction.row === 1) {
    rowPath = rowPath.reverse();
  }
  return {
    rowPath,
    columnPath
  };
};

然后就要寻找目标位置,进行移动
定义一个next元素,使用循环使其定位一步一步加,并且每加一次就调用一次get方法和Outofrange方法
如果get方法有返回值说明此处有方块存在,或者判定是否出范围

// 寻找移动方向目标位置
Manager.prototype.getNearestAvaibleAim = function(aim, direction) {
    // 位置 + 方向向量的计算公式
    function addVector(position, direction) {
        return {
            row: position.row + direction.row,
            column: position.column + direction.column
        };
    }
    aim = addVector(aim, direction);

    // 获取grid中某个位置的元素
    let next = this.grid.get(aim);

    // 如果next元素存在(也就是此目标位置已经有Tile),或者是超出游戏边界,则跳出循环。目的:就是找到最后一个空白且不超过边界的方格
    while (!this.grid.outOfRange(aim) && !next) {
        aim = addVector(aim, direction);
        next = this.grid.get(aim);
    }

    // 这时候的aim总是多计算了一步,因此我们还原一下
    aim = {
        row: aim.row - direction.row,
        column: aim.column - direction.column
    };

    return {
        aim,
        next
    };
};

移动的整体思路:
根据键盘返回的操作,进行相应顺序的遍历
遍历的过程中,发现此处有Tile,则对他进行移动
根据方向,获取目标位置
定义一个moved变量,只要进行移动,就重新渲染

// 移动核心逻辑
Manager.prototype.listenerFn = function(direction) {
    // 定义一个变量,判断是否引起移动
    let moved = false;

    const { rowPath, columnPath } = this.getPaths(direction);
    for (let i = 0; i < rowPath.length; i++) {
        for (let j = 0; j < columnPath.length; j++) {
            const position = { row: rowPath[i], column: columnPath[j] };
            const tile = this.grid.get(position);
            if (tile) {
                // 当此位置有Tile的时候才进行移动
                const { aim, next } = this.getNearestAvaibleAim(position, direction);

                // 区分合并和移动,当next值和tile值相同的时候才进行合并
                if (next && next.value === tile.value) {
                    // 合并位置是next的位置,合并的value是tile.value * 2
                    const merged = new Tile(
                        {
                            row: next.row,
                            column: next.column
                        },
                        tile.value * 2
                    );
                    //分数等于原分数加新合成的数字
                    this.score += merged.value;
                    //将合并以后节点,加入grid
                    this.grid.add(merged);
                    //在grid中删除原始的节点
                    this.grid.remove(tile);
                    //判断游戏是否获胜
                    if (merged.value === this.aim) {
                        this.status = 'WIN';
                    }
                    merged.mergedTiles = [tile, next];
                    tile.updatePosition({ row: next.row, column: next.column });
                    moved = true;
                } else {
                    this.moveTile(tile, aim);
                    moved = true;
                }
            }
        }
    }

方块合并逻辑:
移动到无法移动并且下一个位置的value和当前一样
有移动,原来的地方的方块就要删除
然后新的方块value值x2

Grid.prototype.remove = function(tile) {
  this.cells[tile.row][tile.column] = null;
};

在之前的确定目标位置的时候,是设置了两个变量
一个aim,用于确认目标位置
一个next,用于确认下一个位置
合并时就可以使用nexr.value*2来进行增值
2048初见雏形

  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值