微信跳一跳小游戏实战开发(二) - 开发前物理引擎的学习 - ammo.js

微信跳一跳小游戏实战开发(二) - 开发前物理引擎的学习 - ammo.js
yicheng
发布于

接上一篇。

把例子的代码拷贝过来之后,先让代码起通。因为对ammojs不熟悉,所以要理解和学习一下ammojs。

之前有学习过2D的物理引擎,物理引擎通常会比较相似。

THREE.JS的话,因为我已经是非常熟悉了,所以这里的相关代码比较容易理解。

微信跳一跳小游戏实战开发(二) - 开发前物理引擎的学习 - ammo.js

代码拷贝过来跑通之后,开始理清代码的主要逻辑,并打好注释如下:

<script src="/js/lib/threejs/three.min.js"></script>
<script src="/js/lib/threejs/ammo.js"></script>
<script src="/js/lib/threejs/OrbitControls.js"></script>
<script src="/js/lib/threejs/Detector.js"></script>
<script src="/js/lib/threejs/stats.min.js"></script>
<script>
var game_div = document.getElementById('js-game');

// Detects webgl
// 检测浏览器是否支持webgl
if (!Detector.webgl) {
Detector.addGetWebGLMessage({
parent: game_div
});
}

// Heightfield parameters
// 这里terrain开头的是表示那个波浪形的地形的配置参数
var terrainWidthExtents = 100;  //宽
var terrainDepthExtents = 100;  //深
var terrainWidth = 128;     //对宽的网格进行细分
var terrainDepth = 128;     //网格细分等级
var terrainHalfWidth = terrainWidth / 2;
var terrainHalfDepth = terrainDepth / 2;
var terrainMaxHeight = 8;   //这里是调节地形波浪的峰值
var terrainMinHeight = -2;  //这里是地形波浪的谷值

// Graphics variables
// three.js中的变量
var container, stats;
var camera, controls, scene, renderer;
var terrainMesh, texture;
var clock = new THREE.Clock();

// Physics variables
// 物理引擎的变量
var collisionConfiguration; // new ammo.btDefaultCollisionConfiguration()
var dispatcher; //new Ammo.btCollisionDispatcher(collisionConfiguration);
var broadphase; //暂时不知道啥作用
var solver;     //负责解算 
// 最后上面这四个对象创建一个 Ammojs的物理世界
var physicsWorld;   //new Ammo.btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
var terrainBody;
var dynamicObjects = [];    //把three.js创建的3D对象放在这个数组里边,每次render之前,把Object3D对象的位置和朝向信息从物理刚体对象里边拷贝过来。
var transformAux1 = new Ammo.btTransform();

var heightData = null;
var ammoHeightData = null;

var time = 0;
var objectTimePeriod = 3;   //3秒的间隔
var timeNextSpawn = time + objectTimePeriod;    //物品生成的时间间隔
var maxNumObjects = 30;     //这里是设置最大的物品生成数量

// - Main code -
init();
animate();

function init() {

// 用于生成波浪形的高程数据
heightData = generateHeight(terrainWidth, terrainDepth, terrainMinHeight, terrainMaxHeight);

initGraphics();

initPhysics();

}

// 这个函数里边主要是对three.js的变量进行初始化
// 场景创建,渲染器,摄像头,地面模型等
// 这里我轻车熟路所以打少一点注释
function initGraphics() {

container = game_div;

renderer = new THREE.WebGLRenderer();
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;

container.innerHTML = "";

container.appendChild(renderer.domElement);

stats = new Stats();    //这里是网页左上角显示帧率系统状态等的插件
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = '0px';
container.appendChild(stats.domElement);

// THREEJS有几种的相机  PerspectiveCamera这个是透视摄像机
// 跳一跳里边很明显是用的 OrthographicCamera(正交相机)
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.2, 2000);

scene = new THREE.Scene();
scene.background = new THREE.Color(0xbfd1e5);

camera.position.y = heightData[terrainHalfWidth + terrainHalfDepth * terrainWidth] * (terrainMaxHeight - terrainMinHeight) + 5;

camera.position.z = terrainDepthExtents / 2;
camera.lookAt(new THREE.Vector3(0, 0, 0));

controls = new THREE.OrbitControls(camera); //相机控制器


//创建一个平面的模型
var geometry = new THREE.PlaneBufferGeometry(terrainWidthExtents, terrainDepthExtents, terrainWidth - 1, terrainDepth - 1);
geometry.rotateX(-Math.PI / 2);

//然后对这个平面模型的点的高度进行调节    假如这里高度不调节,那么将会显示成一个平面
var vertices = geometry.attributes.position.array;
for (var i = 0, j = 0, l = vertices.length; i < l; i++ , j += 3) {
// j + 1 because it is the y component that we modify
vertices[j + 1] = heightData[i];
}

//调节完这个平面的点的高度之后,对这个BufferGeometry的顶点法线重新计算一下
geometry.computeVertexNormals();

var groundMaterial = new THREE.MeshPhongMaterial({ color: 0xC7C7C7 });
terrainMesh = new THREE.Mesh(geometry, groundMaterial);
terrainMesh.receiveShadow = true;
terrainMesh.castShadow = true;

scene.add(terrainMesh);

var textureLoader = new THREE.TextureLoader();
textureLoader.load("textures/grid.png", function (texture) {
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(terrainWidth - 1, terrainDepth - 1);
groundMaterial.map = texture;
groundMaterial.needsUpdate = true;

});

// 径向光
var light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(100, 100, 50);
light.castShadow = true;    //产生阴影
var dLight = 200;
var sLight = dLight * 0.25;
light.shadow.camera.left = -sLight;
light.shadow.camera.right = sLight;
light.shadow.camera.top = sLight;
light.shadow.camera.bottom = -sLight;

light.shadow.camera.near = dLight / 30;
light.shadow.camera.far = dLight;

light.shadow.mapSize.x = 1024 * 2;
light.shadow.mapSize.y = 1024 * 2;

scene.add(light);


window.addEventListener('resize', onWindowResize, false);

}

function onWindowResize() {

camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();

renderer.setSize(window.innerWidth, window.innerHeight);

}

function initPhysics() {

// Physics configuration
// 创建和配置物理世界

collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
broadphase = new Ammo.btDbvtBroadphase();
solver = new Ammo.btSequentialImpulseConstraintSolver();
physicsWorld = new Ammo.btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration);
physicsWorld.setGravity(new Ammo.btVector3(0, -6, 0));  //设置重力系数

// Create the terrain body
// 创建

var groundShape = this.createTerrainShape(heightData);
var groundTransform = new Ammo.btTransform();
groundTransform.setIdentity();
// Shifts the terrain, since bullet re-centers it on its bounding box.
groundTransform.setOrigin(new Ammo.btVector3(0, (terrainMaxHeight + terrainMinHeight) / 2, 0));
var groundMass = 0;     //质量  质量设置成0就可以不会掉落了
var groundLocalInertia = new Ammo.btVector3(0, 0, 0);   //惯性
var groundMotionState = new Ammo.btDefaultMotionState(groundTransform);     //运动状态 大概是动量
var groundBody = new Ammo.btRigidBody(new Ammo.btRigidBodyConstructionInfo(groundMass, groundMotionState, groundShape, groundLocalInertia));
physicsWorld.addRigidBody(groundBody);

}

//用户产生波浪形的高程数据
function generateHeight(width, depth, minHeight, maxHeight) {

// Generates the height data (a sinus wave)

var size = width * depth;
var data = new Float32Array(size);

var hRange = maxHeight - minHeight;
var w2 = width / 2;
var d2 = depth / 2;
var phaseMult = 12;

var p = 0;
for (var j = 0; j < depth; j++) {
for (var i = 0; i < width; i++) {

var radius = Math.sqrt(
Math.pow((i - w2) / w2, 2.0) +
Math.pow((j - d2) / d2, 2.0));

var height = (Math.sin(radius * phaseMult) + 1) * 0.5 * hRange + minHeight;

data[p] = height;

p++;
}
}

return data;

}

function createTerrainShape() {

// This parameter is not really used, since we are using PHY_FLOAT height data type and hence it is ignored
var heightScale = 1;

// Up axis = 0 for X, 1 for Y, 2 for Z. Normally 1 = Y is used.
var upAxis = 1;

// hdt, height data type. "PHY_FLOAT" is used. Possible values are "PHY_FLOAT", "PHY_UCHAR", "PHY_SHORT"
var hdt = "PHY_FLOAT";

// Set this to your needs (inverts the triangles)
var flipQuadEdges = false;

// Creates height data buffer in Ammo heap
ammoHeightData = Ammo._malloc(4 * terrainWidth * terrainDepth);

// Copy the javascript height data array to the Ammo one.
var p = 0;
var p2 = 0;
for (var j = 0; j < terrainDepth; j++) {
for (var i = 0; i < terrainWidth; i++) {

// write 32-bit float data to memory
Ammo.HEAPF32[ammoHeightData + p2 >> 2] = heightData[p];

p++;

// 4 bytes/float
p2 += 4;
}
}

// Creates the heightfield physics shape
var heightFieldShape = new Ammo.btHeightfieldTerrainShape(
terrainWidth,
terrainDepth,
ammoHeightData,
heightScale,
terrainMinHeight,
terrainMaxHeight,
upAxis,
hdt,
flipQuadEdges
);

// Set horizontal scale
var scaleX = terrainWidthExtents / (terrainWidth - 1);
var scaleZ = terrainDepthExtents / (terrainDepth - 1);
heightFieldShape.setLocalScaling(new Ammo.btVector3(scaleX, 1, scaleZ));

heightFieldShape.setMargin(0.05);

return heightFieldShape;

}

// 随机生成一个物品从场景的高处掉落
function generateObject() {

var numTypes = 4;
var objectType = Math.ceil(Math.random() * numTypes);

var threeObject = null;
var shape = null;

var objectSize = 3;
var margin = 0.05;

switch (objectType) {
case 1:
// Sphere
var radius = 1 + Math.random() * objectSize;
threeObject = new THREE.Mesh(new THREE.SphereGeometry(radius, 20, 20), createObjectMaterial());
shape = new Ammo.btSphereShape(radius);
shape.setMargin(margin);
break;
case 2:
// Box
var sx = 1 + Math.random() * objectSize;
var sy = 1 + Math.random() * objectSize;
var sz = 1 + Math.random() * objectSize;
threeObject = new THREE.Mesh(new THREE.BoxGeometry(sx, sy, sz, 1, 1, 1), createObjectMaterial());
shape = new Ammo.btBoxShape(new Ammo.btVector3(sx * 0.5, sy * 0.5, sz * 0.5));
shape.setMargin(margin);
break;
case 3:
// Cylinder
var radius = 1 + Math.random() * objectSize;
var height = 1 + Math.random() * objectSize;
threeObject = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, height, 20, 1), createObjectMaterial());
shape = new Ammo.btCylinderShape(new Ammo.btVector3(radius, height * 0.5, radius));
shape.setMargin(margin);
break;
default:
// Cone
var radius = 1 + Math.random() * objectSize;
var height = 2 + Math.random() * objectSize;
threeObject = new THREE.Mesh(new THREE.CylinderGeometry(0, radius, height, 20, 2), createObjectMaterial());
shape = new Ammo.btConeShape(radius, height);
break;
}

//设置物体的坐标
threeObject.position.set((Math.random() - 0.5) * terrainWidth * 0.6, terrainMaxHeight + objectSize + 2, (Math.random() - 0.5) * terrainDepth * 0.6);

//质量
var mass = objectSize * 5;
var localInertia = new Ammo.btVector3(0, 0, 0);     //惯性
shape.calculateLocalInertia(mass, localInertia);    //应该是把shape对象里边的一些参数计算一下,比方说计算一下质量的倒数,惯性的倒数   在需要的时候不用每一次的计算了
var transform = new Ammo.btTransform();
transform.setIdentity();
var pos = threeObject.position;
transform.setOrigin(new Ammo.btVector3(pos.x, pos.y, pos.z));
var motionState = new Ammo.btDefaultMotionState(transform);
var rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, shape, localInertia);
var body = new Ammo.btRigidBody(rbInfo);

threeObject.userData.physicsBody = body;    //把ammo创建的刚体放在three.js3D对象的userData里边

threeObject.receiveShadow = true;
threeObject.castShadow = true;

scene.add(threeObject);             //
dynamicObjects.push(threeObject);   //放入数组中

physicsWorld.addRigidBody(body);    //将物体加入物理世界中进行模拟



}

function createObjectMaterial() {
var c = Math.floor(Math.random() * (1 << 24));
return new THREE.MeshPhongMaterial({ color: c });
}

function animate() {

requestAnimationFrame(animate);

render();
stats.update(); //这个是更新网页左上角那个显示帧率等信息的stats对象

}

function render() {

var deltaTime = clock.getDelta();
// deltaTime 大概是16-17毫秒  但是是以秒为单位的

if (dynamicObjects.length < maxNumObjects && time > timeNextSpawn) {    //当数量没超过最大设置数量 同时 时间间隔大于3秒
generateObject();   //创建一个对象
timeNextSpawn = time + objectTimePeriod;    //设置下一次的添加时间
}

updatePhysics(deltaTime);   //进行物理场景模拟 同时把模拟数据同步到three.js的场景中

renderer.render(scene, camera);     //渲染器把场景渲染到画布上

time += deltaTime;

}

function updatePhysics(deltaTime) {

//进行仿真模拟 
// 参数10应该是运算时插入的中间帧
physicsWorld.stepSimulation(deltaTime, 10);

// Update objects
for (var i = 0, il = dynamicObjects.length; i < il; i++) {
var objThree = dynamicObjects[i];
var objPhys = objThree.userData.physicsBody;
var ms = objPhys.getMotionState();
if (ms) {

ms.getWorldTransform(transformAux1);
var p = transformAux1.getOrigin();  //物体的原点
var q = transformAux1.getRotation();    //物体的朝向
objThree.position.set(p.x(), p.y(), p.z()); //更新物体的位置
objThree.quaternion.set(q.x(), q.y(), q.z(), q.w());    //四元数组可以设置物体的朝向

}
}
}


</script>

下一步就是修改代码,然后把代码整理成自己想要的形势。详见下一篇。

本文地址:茜文博客 >>微信跳一跳小游戏实战开发(二) - 开发前物理引擎的学习 - ammo.js

转载请注明出处!!!3Q~~~