【three.js学习笔记(2)】完成一个3D游戏
本文借鉴了The Aviator的玩法和部分代码,最终效果如下。
1 加载模型和动画
参考学习笔记1中的方法,将海豚和鲨鱼模型及动画加载到three.js中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 对象构造器
let playerObj;
const mixers = [];
// 游戏人物
const Player = function () {
const isExist = playerObj ? true : false;
const loader = new THREE.FBXLoader();
loader.load('assets/models/Player' + OPTIONS.player + '.fbx', function (object) {
object.mixer = new THREE.AnimationMixer(object);
mixers.push(object.mixer);
const action = object.mixer.clipAction(object.animations[0]);
action.play();
// 侧面面对镜头
object.rotation.y = 1.57;
object.position.y = 50;
object.position.z = 0;
const scale = OPTIONS.objScale[OPTIONS.player];
object.scale.set(scale, scale, scale);
// 加入场景
if (isExist) {
scene.remove(playerObj)
playerObj = object;
scene.add(object);
} else {
playerObj = object;
scene.add(object);
// 载入obj后开始第一次动画循环
loop();
}
});
};
游戏开始时玩家需要选择两个游戏人物中的一个,变量isExist用于判断之前是否已载入某个模型,变量scale根据不同的角色调节模型的大小。载入完毕后开始动画循环。
2 加入海底滚筒
海底加入滚筒并不断向后滚动,模拟主角游动的场景。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41// 海底
const Sea = function () {
// 锥:上底半径,下底半径,高度,圆面划分,高度划分
const geom = new THREE.CylinderGeometry(OPTIONS.seaRadius, OPTIONS.seaRadius, 200, 40, 10);
// 滚动
geom.applyMatrix(new THREE.Matrix4().makeRotationX(-Math.PI/2));
geom.mergeVertices();
// 增加波动
this.waves = [];
for (let i=0; i < geom.vertices.length; i++) {
this.waves.push({
y: geom.vertices[i].y,
x: geom.vertices[i].x,
z: geom.vertices[i].z,
ang: Math.random() * Math.PI * 2,
amp: 5 + Math.random() * 15,
speed: 0.016 + Math.random() * 0.032
});
};
// 材质
const mat = new THREE.MeshPhongMaterial({
color: COLORS.sand
});
mat.flatShading = true;
this.mesh = new THREE.Mesh(geom, mat);
this.mesh.position.y = -600;
this.mesh.position.z = -100;
scene.add(this.mesh);
}
Sea.prototype.wave = function () {
const verts = this.mesh.geometry.vertices;
for (let i=0; i < verts.length; i++) {
const vprops = this.waves[i];
verts[i].x = vprops.x + Math.cos(vprops.ang) * vprops.amp;
verts[i].y = vprops.y + Math.sin(vprops.ang) * vprops.amp;
vprops.ang += vprops.speed;
}
this.mesh.geometry.verticesNeedUpdate = true;
sea.mesh.rotation.z += 0.005;
}
3 引入绿色小球
绿色小球触碰后消失,并增加得分。绿球起始在Math.PI / 3位置处生成,当全部被主角吃掉或运动至-Math.PI / 3处时,上一组绿色小球消失,并重新生成下一组。对每个绿色小球,每一帧除自转公转外,还需判断是否与主角碰撞。若发生碰撞,绿色小球消失,启动爆炸的粒子效果并增加得分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80// 绿色小球
const GreenBalls = function () {
this.generate();
}
GreenBalls.prototype.generate = function () {
// 删除已有mesh
if (this.meshArr) {
for (let i = 0; i < this.meshArr.length; i++) {
this.mesh.remove(this.meshArr[i]);
}
}
// 生成一排绿色球
this.mesh = new THREE.Object3D();
this.meshArr = [];
const geom = new THREE.TetrahedronGeometry(10, 0);
const mat = new THREE.MeshPhongMaterial({
color: 0x00CD66,
shininess: 0,
specular: 0xFFFFFF
});
mat.flatShading = true;
// 每次几个球
const count = Math.floor(2 + Math.random() * 6);
const ANGLE = Math.PI / 50;
// 高度范围620-800
const h = 620 + Math.random() * 180;
for (let i = 0; i < count; i++) {
const greenBallMesh = new THREE.Mesh(geom.clone(), mat);
greenBallMesh.position.x = Math.sin(ANGLE * i + Math.PI / 3) * h;
greenBallMesh.position.y = Math.cos(ANGLE * i + Math.PI / 3) * h;
greenBallMesh.position.z = 0;
greenBallMesh.rotation.y = Math.random() * Math.PI;
greenBallMesh.rotation.z = Math.random() * Math.PI;
greenBallMesh.angle = ANGLE * i + Math.PI / 3;
greenBallMesh.h = h;
this.mesh.add(greenBallMesh);
this.meshArr.push(greenBallMesh);
}
this.mesh.position.y = -600;
scene.add(this.mesh);
};
GreenBalls.prototype.update = function (deltaTime) {
const count = this.meshArr.length;
for (let i = 0; i < count; i++) {
// 自转
this.meshArr[i].rotation.x += 3 * deltaTime * OPTIONS.angleSpeed;
this.meshArr[i].rotation.z += 2 * deltaTime * OPTIONS.angleSpeed;
// 公转,逆时针旋转
this.meshArr[i].angle -= deltaTime * OPTIONS.angleSpeed;
const angle = this.meshArr[i].angle;
const h = this.meshArr[i].h;
this.meshArr[i].position.x = Math.sin(angle) * h;
this.meshArr[i].position.y = Math.cos(angle) * h;
// 判断是否碰撞
let diffX = this.meshArr[i].position.x - playerObj.position.x;
diffX = diffX > 0 ? diffX : -diffX;
let diffY = this.meshArr[i].position.y - (playerObj.position.y + 600);
diffY = diffY > 0 ? diffY : -diffY;
if (diffX < OPTIONS.objBoundary[OPTIONS.player][0] &&
diffY < OPTIONS.objBoundary[OPTIONS.player][1]) {
particles.generate(15, playerObj.position.clone(), 0x009999, 3);
this.mesh.remove(this.meshArr[i]);
this.meshArr.splice(i, 1);
// this.meshArr.shift();
// 计分
OPTIONS.score += OPTIONS.greenBallScore;
// 玩家选择鲨鱼,单独加一定分数
if (OPTIONS.player === 1) {
OPTIONS.score += 5;
}
break;
}
}
// 绿球都被吃或超过左边界
if (this.meshArr.length === 0 || this.meshArr[0].angle < -Math.PI / 3) {
this.generate();
}
};
4 引入红色小球
红色小球触碰后爆炸,主角受冲击后退并减少得分。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65// 红色小球
const RedBall = function () {
this.generate();
}
RedBall.prototype.generate = function () {
// 删除已有mesh
if (this.meshTemp) {
this.mesh.remove(this.meshTemp);
}
this.mesh = new THREE.Object3D();
// 生成一排绿色球
const geom = new THREE.TetrahedronGeometry(8, 2);
const mat = new THREE.MeshPhongMaterial({
color: COLORS.red,
shininess: 0,
specular: 0xFFFFFF
});
mat.flatShading = true;
const h = 620 + Math.random() * 180;
const mesh = new THREE.Mesh(geom, mat);
mesh.position.x = Math.sin(Math.PI / 3) * h;
mesh.position.y = -600 + Math.cos(Math.PI / 3) * h;
mesh.position.z = 0;
mesh.rotation.y = Math.random() * Math.PI;
mesh.rotation.z = Math.random() * Math.PI;
mesh.angle = Math.PI / 3;
mesh.h = h;
this.meshTemp = mesh;
this.mesh.add(mesh);
scene.add(this.mesh);
};
RedBall.prototype.update = function (deltaTime) {
// 自转
this.meshTemp.rotation.x -= 2 * deltaTime * OPTIONS.angleSpeed;
this.meshTemp.rotation.z -= 3 * deltaTime * OPTIONS.angleSpeed;
// 公转,逆时针旋转,绿色小球的1.8倍
this.meshTemp.angle -= 1.8 * deltaTime * OPTIONS.angleSpeed;
const angle = this.meshTemp.angle;
const h = this.meshTemp.h;
this.meshTemp.position.x = Math.sin(angle) * h;
this.meshTemp.position.y = -600 + Math.cos(angle) * h;
// 判断是否碰撞
let diffX = this.meshTemp.position.x - playerObj.position.x;
diffX = diffX > 0 ? diffX : -diffX;
let diffY = this.meshTemp.position.y - (playerObj.position.y);
diffY = diffY > 0 ? diffY : -diffY;
// console.log(diffX + " " + diffY)
if (diffX < OPTIONS.objBoundary[OPTIONS.player][0] &&
diffY < OPTIONS.objBoundary[OPTIONS.player][1]) {
// 发生碰撞
this.mesh.remove(this.meshTemp);
this.meshTemp = undefined;
playerObj.position.x -= 20;
playerObj.position.y -= 5;
particles.generate(15, playerObj.position.clone(), COLORS.red, 3);
// 计分
OPTIONS.score -= OPTIONS.redBallScore;
}
// 红球被吃或超过左边界
if (this.meshTemp === undefined || this.meshTemp.angle < -Math.PI / 3) {
this.generate();
}
};
5 粒子效果
主角触碰绿色小球或红色小球时,小球消失并产生爆炸的粒子效果,如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49const Particle = function() {
const geom = new THREE.TetrahedronGeometry(3, 0);
const mat = new THREE.MeshPhongMaterial({
color: 0x009999,
shininess: 0,
specular: 0xFFFFFF
});
mat.flatShading = true;
this.mesh = new THREE.Mesh(geom,mat);
}
Particle.prototype.animate = function(position, color, scale) {
this.mesh.material.color = new THREE.Color(color);
this.mesh.material.needsUpdate = true;
this.mesh.scale.set(scale, scale, scale);
// 向周围四散
const speed = 0.6 + Math.random() * 0.2;
TweenMax.to(this.mesh.rotation, speed, {
x: Math.random() * 12,
y: Math.random() * 12
});
TweenMax.to(this.mesh.scale, speed, {
x: 0.1,
y: 0.1,
z: 0.1
});
TweenMax.to(this.mesh.position, speed, {
x: position.x + (Math.random() * 2 - 1) * 50,
y: position.y + (Math.random() * 2 - 1) * 50,
delay: Math.random() * 0.1,
ease: Power2.easeOut
});
}
Particles = function () {
this.mesh = new THREE.Object3D();
scene.add(this.mesh)
}
Particles.prototype.generate = function(count, position, color, scale) {
for (var i = 0; i < count; i++) {
const particle = new Particle();
particle.mesh.visible = true;
particle.mesh.position.x = position.x + OPTIONS.objBoundary[OPTIONS.player][0];
particle.mesh.position.y = position.y;
this.mesh.add(particle.mesh);
particle.animate(position, color, scale);
}
}
首先定义单个粒子的形状和发生爆炸时的动画效果animate。每个粒子的爆炸方向、大小、速度均随机。之后定义粒子效果和生成方法,每次需要爆炸效果时,调用粒子效果的生成方法即可。生成方法的四个参数count, position, color, scale分别代表粒子个数,爆炸时主角位置,爆炸粒子颜色及大小。
6 动画循环
1 | // 动画循环 |
每次循环先判断当前游戏状态,start为开始界面,play为游戏界面。开始游戏时根据鼠标位置改变主角运动方向。播放主角动画,海洋波动动画。调用绿色小球、红色小球的update方法,判断分数是否大于0并更新分数。
7 交互
1 | // 交互 |
主要有三种交互,一是游戏开始界面鼠标点击切换主角,二是开始界面按任意键开始游戏,三是游戏界面定位鼠标位置。
8 其他
在three.js中创建场景、光照、各种object并初始化。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77// 创建场景
function createScene() {
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
const fieldOfView = 60;
const aspectRatio = WIDTH / HEIGHT;
const nearPlane = 1;
const farPlane = 10000;
// 相机
camera = new THREE.PerspectiveCamera(
fieldOfView,
aspectRatio,
nearPlane,
farPlane
);
camera.position.x = 0;
camera.position.z = 220;
// camera.position.z = 2000;
camera.position.y = 100;
// 场景
scene = new THREE.Scene();
// 雾气效果:颜色,近点,远点
// scene.fog = new THREE.Fog(COLORS.blue, 100, 950);
// 渲染
renderer = new THREE.WebGLRenderer({alpha: true, antialias: true});
renderer.setSize(WIDTH, HEIGHT);
// renderer.shadowMap.enabled = true;
// 加入DOM
container = document.getElementById('webgl');
container.appendChild(renderer.domElement);
window.addEventListener('resize', handleWindowResize, false);
}
// 屏幕自适应
function handleWindowResize() {
HEIGHT = window.innerHeight;
WIDTH = window.innerWidth;
renderer.setSize(WIDTH, HEIGHT);
camera.aspect = WIDTH / HEIGHT;
camera.updateProjectionMatrix();
}
// 光照
function createLights() {
// 直射光
const shadowLight = new THREE.DirectionalLight(0xffffff, 0.9);
shadowLight.position.set(150, 350, 350);
scene.add(shadowLight);
}
// 创建对象
function createObject() {
// 玩家
player = new Player();
// 海洋滚筒
sea = new Sea();
// 绿色球
greenBalls = new GreenBalls();
// 红色球
redBall = new RedBall();
// 粒子效果
particles = new Particles();
}
function init() {
document.addEventListener('mousedown', handleMouseClick, false);
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('mousemove', handleMouseMove, false);
scoreDOM = document.getElementById('score');
describeDOM = document.getElementById('describe');
createScene();
createLights();
createObject();
}
window.addEventListener('load', init, false);
index.html文件如下,使用CSS设置渐变背景色。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Game</title>
<style>
body {
margin: 0;
}
#score {
position: absolute;
top: 5%;
width: 100%;
z-index: 10;
text-align: center;
font-size: 3rem;
color: #eeeeee;
}
#describe {
position: absolute;
top: 40%;
width: 100%;
z-index: 10;
text-align: center;
font-size: 2rem;
color: #eeeeee;
}
#webgl {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
background: -webkit-linear-gradient(#55f0f6, #48a0c9);
background: linear-gradient(#55f0f6, #48a0c9);
}
</style>
</head>
<body>
<div id="score"></div>
<div id="describe"></div>
<div id="webgl"></div>
<script src="js/lib/three.min.js"></script>
<script src="js/lib/FBXLoader.js"></script>
<script src="js/lib/inflate.min.js"></script>
<script src="js/lib/TweenMax.min.js"></script>
<script src="js/game.js" /></script>
</body>
</html>
完整程序见我的github。