一个简单的threejs3D推箱子小游戏(2)——场景生成

1,005 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第32天,点击查看活动详情

上一篇文章中讲到了这个推箱子小游戏的初始化部分,今天我们来看看场景的生成部分。

场景生成

当年写的代码居然还有面向对象的方式在里面,而且还用的是Funcion来写的一个class(应该是当时还没太听说过es6),主要就是三个部分,基本方块类、人物方块类和终点方块类。

基本方块类

基本方块类的代码如下:

/*
  基本方块类
 */
function Cube(state, row, column, materialurl, x, y, z) {
  this.state = state; //状态 1墙体 2箱子
  this.row = row; //所在行
  this.column = column; //所在列
  if (state == 1 || state == 2) {
    var cubegeometry = new THREE.BoxGeometry(10, 10, 10); //方块大小固定
    if (materialurl == "crate") {
      var cubematerial = new THREE.MeshBasicMaterial({
        map: textureloader.loadTexture('img/crate.jpg')
      });
    } else {
      var cubematerial = new THREE.MeshBasicMaterial({
        map: textureloader.loadTexture('img/bricks.jpg')
      });
    }
    var cube = new THREE.Mesh(cubegeometry, cubematerial);
    cube.position.set(x, y, z);
    this.cube = cube; //将网孔对象传出以便移动
    scene.add(cube);
  }
}

基本方块类主要包括两种方块,墙体和箱子,首先构建一个正方体BoxGeometry对象,,之后根据不同的类型加载不同的贴图生成MeshBasicMaterial对象在正方体的六个面都贴上图片,最后让二者结合生成Mesh对象后,将其添加到场景中,并将这个对象暴露在Cube对象的cube属性中,以便后续的移动操作。

image.png

人物方块类

人物方块类的代码如下:

/*
  人物方块类
 */
function PersonCube(row, column) {
  this.state = 3; //3代表人物
  this.row = row;
  this.column = column;
  var loader = new THREE.STLLoader();
  var ghostMaterial = new THREE.MeshPhongMaterial({
    color:0xffffff
  });
  loader.load("img/Fantasma.stl", function(obj) {
    var cube = new THREE.Mesh(obj, ghostMaterial);
    cube.rotateX(Math.PI / 2 * 3);
    cube.position.set((column - 1) * 10 + 5, 0, (row - 1) * 10 + 5);
    cube.scale.set(3, 3, 3);
    scene.add(cube);
    ghost.cube = cube;
    document.getElementById("white").style.display="none";
  })
  this.isavailable = function(direction) {
    row = this.row;
    column = this.column;
    var availarr = [0, 4]; //可以直接走的标记
    switch (direction) {
      case "w":
        if (cubes[row - 1] && availarr.includes(cubes[row - 1][column].state)) {
          return true;
        }
        if (cubes[row - 2] && availarr.includes(cubes[row - 2][column].state)) {
          if (cubes[row - 1][column].state == 2) {
            return true;
          }
        }
        return false;
      case "s":
        if (availarr.includes(cubes[row + 1] && cubes[row + 1][column].state)) {
          return true;
        }
        if (availarr.includes(cubes[row + 2] && cubes[row + 2][column].state)) {
          if (cubes[row + 1][column].state == 2) {
            return true;
          }
        }
        return false;
      case "a":
        if (availarr.includes(cubes[row][column - 1] && cubes[row][column - 1].state)) {
          return true;
        }
        if (availarr.includes(cubes[row][column - 2] && cubes[row][column - 2].state)) {
          if (cubes[row][column - 1].state == 2) {
            return true;
          }
        }
        return false;
      case "d":
        if (availarr.includes(cubes[row][column + 1] && cubes[row][column + 1].state)) {
          return true;
        }
        if (availarr.includes(cubes[row][column + 2] && cubes[row][column + 2].state)) {
          if (cubes[row][column + 1].state == 2) {
            return true;
          }
        }
        return false;
      default:
        console.log("direction error");
    }
  }
}

与基础方块类不同的是,人物类不是采用了正方体进行贴图,而是加载了一个stl模型,再构建一个MeshPhongMaterial对象为其上色,上色后依然将二者组合为Mesh对象,添加到场景中。

此外,人物方块多了一个属性isAvailable,用来判断在各个方向是否可以移动。

  • 前方无箱子和墙方块,可以移动
  • 前方有箱子方块,则进一步判断下一个位置是否有方块,若没有,则可以移动
  • 其他情况下不可移动

image.png

终点方块类

终点方块类的代码如下:

/*
  终点方块类
 */
function DesCube(i, j) {
  desCubes = new Array();//清空完成数组
  var textureloader = new THREE.TextureLoader();
  var loader = new THREE.STLLoader();
  var ghostMaterial = new THREE.MeshBasicMaterial({
    color: 0x2033a4,
  });
  loader.load("img/Base_luz.stl", function(obj) {
    var cube = new THREE.Mesh(obj, ghostMaterial);
    cube.rotateX(Math.PI / 2 * 3);
    cube.position.set((j - 1) * 10 + 5, 0, (i - 1) * 10 + 5);
    cube.scale.set(3, 3, 3);
    scene.add(cube);
    // boxhelper = new THREE.BoxHelper(cube);
    // scene.add(boxhelper);
    desCubes.push({
      state: 0, //0代表未完成 1代表完成
      position: {
        row: i,
        column: j
      }
    });
  });
}
DesCube.prototype.checkcube = function(row, column) {
  for (var i = 0; i < desCubes.length; i++) { //判断单个箱子是否完成
    if (row == desCubes[i].position.row && column == desCubes[i].position.column) {
      desCubes[i].state = 1;
      return true; //表示已到位
    }
  }
  return false;
}
DesCube.prototype.checkdes = function() {
  for (var i = 0; i < desCubes.length; i++) {
    if (desCubes[i].position.row == ghost.row && desCubes[i].position.column == ghost.column) { //幽灵若在目标点则证明箱子被推出去了,此点未完成
      desCubes[i].state = 0;
    }
  }
  for (var i = 0; i < desCubes.length; i++) {
    if (desCubes[i].state == 0) {
      return false;
    }
  }
  return true;
};

与人物方块类类似,终点方块也进行了模型的加载和上色,而终点方块多了两个方法,一个是checkcube用以检查当前终点位置是否与箱子位置重合,若重合则表示箱子已经推到位,进行相应处理(将箱子变小);另一个是关卡检查方法checkdes当有箱子推到位时,进行一次关卡的检查,检查是否所有的箱子都已经就为,如果完成则显示通关提示。

image.png

image.png

各个方块的初始化

定义好各个方块的类和方法之后,就可以开始着手初始化关卡了:

/*
   元素绘制
 */
//网格线的绘制
function initmeshline() {
  for (var i = 0; i <= 100; i += 10) {
    //线长度固定
    var lineContainer = new THREE.Geometry();
    lineContainer.vertices.push(new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, 100));
    //与z轴平行的线
    var line = new THREE.Line(lineContainer, new THREE.LineBasicMaterial());
    scene.add(line);
    line.position.set(i, 0, 0);
    // 与x轴平行的线
    var rotateline = new THREE.Line(lineContainer, new THREE.LineBasicMaterial());
    rotateline.rotateY(Math.PI / 2);
    rotateline.position.set(0, 0, i);
    scene.add(rotateline);
  }
}
temparray = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 4, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 2, 0, 2, 4, 1, 0, 0, 1, 4, 0, 2, 3, 1, 1, 1, 0, 0, 1, 1, 1, 1, 2, 1, 0, 0, 0, 0, 0, 0, 0, 1, 4, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
//方块生成,使用二维数组,封装成函数以便地图加载时候调用
function initcube(temparray) {
  scene.visible = true;
  document.body.style.backgroundImage = "url(img/hacker.jpg)"; //恢复网页背景
  cubes = new Array(); //清空之前数据
  var tempindex = 0;
  for (var i = 1; i <= 10; i++) {
    cubes[i] = new Array(); //避免一维数组未初始化时调用二维索引出错
    for (var j = 1; j <= 10; j++) {
      // var index = parseInt(Math.random() * 3);
      var index = temparray[tempindex];
      switch (index) {
        case 4:
          descube = new DesCube(i, j);
          cubes[i][j] = {
            state: 4
          };
          tempindex++;
          break;
        case 3:
          ghost = new PersonCube(i, j);
          cubes[i][j] = {
            state: 3
          }; //更新数组
          tempindex++;
          break;
        case 2:
          cubes[i][j] = new Cube(index, i, j, 'crate', (j - 1) * 10 + 5, 5, (i - 1) * 10 + 5);
          // scene.add(cubes[i][j].cube);
          tempindex++;
          break;
        case 1:
          cubes[i][j] = new Cube(index, i, j, 'bricks', (j - 1) * 10 + 5, 5, (i - 1) * 10 + 5);
          // scene.add(cubes[i][j].cube);
          tempindex++;
          break;
        case 0:
          cubes[i][j] = new Cube(0, i, j);
          tempindex++;
        default:
          continue;
      }
    }
  }
}

首先是进行网格线的绘制,没啥说的,基本就是循环遍历生成Line对象,并将其添加到场景之中。

之后的方块生成方法传入了一个100个元素长度的一维数组,遍历将其生成为二位数组,之后根据类型将对应的方块实例化并保存到cubes二维数组中,以便之后进行移动、判断终点之类的处理。(为什么没有使用scene.add方法将方块对象添加到场景中呢?因为在方块类实例化中就已经完成了这一步骤了)

而之所以这里还要传一个参数,一方面是为了关卡切换,另一方面还加入了自定义关卡的功能,通过这个100个长度的数组可以自定义关卡:

//根据载入数据生成地图
function initmap(arr) {
  if (arr.length == 100) {
    for (var i = 0; i < arr.length; i++) { //字符串转化为整数以便识别
      arr[i] = parseInt(arr[i]);
    }
    for (var i = 0; i < scene.children.length; i++) {
      if (scene.children[i] && scene.children[i].type == "Mesh") {
        scene.remove(scene.children[i]); //移除原有元素
        i--; //防止元素跳过
      }
    }
    step = [];
    stepflag = []; //重置步骤
    temparray = arr; //将数组存入全局变量以便刷新使用
    initcube(temparray); //重新生成元素
    document.getElementById("num").innerHTML = step.length; //更新步骤显示
  } else {
    swal('地图文件格式错误', '请读入正确的地图文件', 'error');
  }
}