上面是我们这节教程最后显示出来的效果。案例实现了人物跟随着移动操作杆进行移动并执行跑步动作,右边的攻击按钮可以实现攻击,短时间内连按可以实现不同的攻击动作。接下来我将和大家一起一步一步的实现这个效果。

场景的搭建

首先,我们需要把舞台搭建出来,先创建scene场景:

创建scene

scene = new THREE.Scene();
scene.background = new THREE.Color(0xa0a0a0);
scene.fog = new THREE.Fog(0xa0a0a0, 1000, 11000);

我们创建了场景,并设置了场景一个灰色的背景色。还设置了场景的雾化效果,这个雾的效果主要是针对于场景的相机的距离实现的,三个值分别是雾的颜色、雾的开始距离、完全雾化距离相机的位置。

创建camera

我们创建了一个与地面呈45度角并朝向原点的相机:

camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 20000);
camera.position.set(0, 800, -800);
camera.lookAt(new THREE.Vector3());

创建灯光

我们创建了两个灯光:照射全局的环境光和可以产生阴影的平衡光。

scene.add(new THREE.AmbientLight(0x444444));light = new THREE.DirectionalLight(0xaaaaaa);
light.position.set(0, 200, 100);
light.lookAt(new THREE.Vector3());light.castShadow = true;
light.shadow.camera.top = 180;
light.shadow.camera.bottom = -180;
light.shadow.camera.left = -180;
light.shadow.camera.right = 180;//告诉平行光需要开启阴影投射
light.castShadow = true;scene.add(light);

创建草地

我们使用平面几何体创建了一个贴有草皮贴图的材质的模型:

var groundTexture = new THREE.TextureLoader().load('../images/grasslight-big.jpg');
groundTexture.wrapS = groundTexture.wrapT = THREE.RepeatWrapping;
groundTexture.repeat.set(25, 25);
groundTexture.anisotropy = 16;
var groundMaterial = new THREE.MeshLambertMaterial({map: groundTexture});
var mesh = new THREE.Mesh(new THREE.PlaneBufferGeometry(20000, 20000), groundMaterial);
mesh.rotation.x = -Math.PI / 2;
mesh.receiveShadow = true;
scene.add(mesh);

到这里,场景、灯光、相机、舞台都已经备齐。接下来我们将请出我们主角naruto登场。

人物模型和动画

添加人物模型

接下来主人公登场,首先我们将模型导入到场景内,注意,案例中的模型比较大,加载和处理需要一定的时间,请小伙伴们耐心等待即可:

var loader = new THREE.FBXLoader();loader.load("../js/models/fbx/Naruto.fbx", function (mesh) {scene.add(mesh);});

我们不单单只是将模型添加到场景,还对模型的阴影和位置做了一下调整:

//设置模型的每个部位都可以投影
mesh.traverse(function (child) {if (child.isMesh) {child.castShadow = true;child.receiveShadow = true;}
});

调整模型的位置,站立在草地上面

mesh.position.y += 110;

设置灯光一直照射模型:

//设置光线焦点模型
light.target = mesh;

添加动画

这个模型里面含有27个骨骼动画,我们可以通过设置不同的动画,来实现一整套的动作来实现相应的比如攻击效果,移动效果等。接下来我们通过模型的数据生成一下所需的动画:

actions = []; //所有的动画数组for (var i = 0; i < mesh.animations.length; i++) {createAction(i);
}function createAction(i) {actions[i] = mixer.clipAction(mesh.animations[i]);gui["action" + i] = function () {for (var j = 0; j < actions.length; j++) {if (j === i) {actions[j].play();}else {actions[j].stop();}}};
}//添加暂停所有动画的按键
gui.stop = function () {for (var i = 0; i < actions.length; i++) {actions[i].stop();}
};

模型加载成功后,我们需要让模型执行一个普通的站立效果:

//第24个动作是鸣人站立的动作
gui["action" + 24]();

添加操作

在案例中,我们主要添加了两种操作:模型位置移动操作和攻击效果。
操作按钮为了方便,直接使用的dom标签模拟出来的。
模型位置移动操作中,我们需要模型的位置的变动和模型的朝向以及修改站立动画和奔跑动画的切换。
攻击效果则是实现攻击并且根据点击速度实现一整套的攻击动作切换。

实现位置移动效果

在实现位置移动效果中,我们为按钮绑定了三个事件:鼠标按下,鼠标移动,鼠标抬起。
在鼠标按下时,我们获取到了当前操作圆盘的中心点的位置,让模型进入跑步动画,绑定了鼠标的移动和抬起事件。重要的是更新模型的移动方向和移动速度。

dop.$(control).on("down", function (event) {event.preventDefault();//获取当前的按钮中心点center.x = window.innerWidth - parseFloat(dop.getFinalStyle(control, "right")) - parseFloat(dop.getFinalStyle(control, "width")) / 2;center.y = window.innerHeight - parseFloat(dop.getFinalStyle(control, "bottom")) - parseFloat(dop.getFinalStyle(control, "height")) / 2;getRadian(event);//鼠标按下切换跑步动作state.skills === 0 && gui["action" + 3]();//给document绑定拖拽和鼠标抬起事件doc.on("move", move);doc.on("up", up);
});

上面的dop类是本人封装的一个兼容多端的事件库,具体详情可以通过 点击这里 查看。
在鼠标移动回调事件中,我们更新模型的移动方向和移动速度。

function move(event) {getRadian(event);
}

最后在鼠标抬起事件中,我们解绑事件,将按键复原,并停止掉模型的移动状态,将模型动画恢复到站立状态。

function up() {doc.remove("move", move);doc.remove("up", up);//按钮复原bar.style.marginTop = 0;barWrap.style.transform = `translate(-50%, -50%) rotate(0deg)`;bar.style.transform = `translate(-50%, -50%) rotate(0deg)`;//设置移动距离为零characterMove(new THREE.Vector2(), 0);//鼠标抬起切换站立状态state.skills === 0 && gui["action" + 24]();
}

三个事件绑定完成后,我们需要将在回调中获得的值求出当前的偏转方向和移动速度:
首先我们获取一下当前鼠标的位置:

if (media === "pc") {mouse.x = event.clientX;mouse.y = event.clientY;
}
else {mouse.x = event.touches[0].clientX;mouse.y = event.touches[0].clientY;
}

根据位置求出距离操作圆盘中心的位置,并保证最大值也不会超出圆盘的半径:

let distance = center.distanceTo(mouse);
distance >= parseFloat(dop.getFinalStyle(control, "width")) / 2 && (distance = parseFloat(dop.getFinalStyle(control, "width")) / 2);

计算出来当前位置和中心的夹角,并修改dom的位置:

//计算两点之间的夹角
mouse.x = mouse.x - center.x;
mouse.y = mouse.y - center.y;//修改操作杆的css样式
bar.style.marginTop = `-${distance}px`;
bar.style.transform = `translate(-50%, -50%) rotate(-${(mouse.angle() / Math.PI * 180 + 90) % 360}deg)`;
barWrap.style.transform = `translate(-50%, -50%) rotate(${(mouse.angle() / Math.PI * 180 + 90) % 360}deg)`;

函数的最后,则调用的characterMove方法,将按钮数据转换成为模型实际需要移动的距离。

//修改当前的移动方向和移动速度
characterMove(mouse.normalize(), distance / (parseFloat(dop.getFinalStyle(control, "width")) / 2));

接下来我们查看一下characterMove方法,在这个方法中,我们计算出了模型每一帧需要移动的距离。这里有一个问题,我们所谓的操作杆向前让模型移动前方,其实是相机朝向的前方。所以我们需要先求出相机的前方矢量,再通过相机的前方矢量为基础,计算出来模型实际方向。
我们首先声明了两个变量,一个是旋转矩阵,另一个是移动矢量:

let direction = new THREE.Matrix4(); //当前移动的旋转矩阵
let move = new THREE.Vector3(); //当前位置移动的距离

characterMove函数内,我们根据相机的四元数获得了旋转矩阵:

//重置矩阵
direction.identity();//通过相机的四元数获取到相机的旋转矩阵
let quaternion = camera.quaternion;
direction.makeRotationFromQuaternion(quaternion);

然后通过旋转矩阵和当前的操作杆的方向通过相乘计算出来实际模型移动的方向:

//获取到操作杆的移动方向
move.x = vector.x;
move.y = 0;
move.z = vector.y;//通过相机方向和操作杆获得最终角色的移动方向
move.applyMatrix4(direction);
move.normalize();

最后,通过比例和方向得出当前模型每一帧移动的距离,因为我们不需要修改模型y轴,所以实际上也只是修改两个轴的位置:

move.x = move.x * ratio * 10;
move.z = move.z * ratio * 10;

我们获取到了模型的每一帧移动的距离,还需要在帧循环中调用:

//如果模型添加成功,则每帧都移动角色位置
if (naruto) {//获取当前位置position.x += move.x;position.z += move.z;//修改模型位置naruto.position.x = position.x;naruto.position.z = position.z;//修改平衡光的位置light.position.x = position.x;light.position.z = position.z + 100;//修改相机位置camera.position.x = position.x;camera.position.z = position.z - 800;
}

当前的模型,灯光,和相机都会跟随移动,实现了,我们上面动图中的模型移动的效果。

实现攻击效果

在实现攻击效果时,我没有只是简单的实现一个普通的攻击,而是直接实现一套连招。
这一套连招是通过五个动作组成,在执行一个攻击动画时如果再次点击了攻击按钮,执行完这个攻击动画将不会切换到站立动画,而是直接切换到连招的下一个攻击动画中。
只要连续点按攻击按钮,模型将完成一整套的动作。实现这个效果,我们只是使用了一个简单的定时器即可实现,接下来我们通过代码了解一下实现过程。

在实现动画前,先设置一个连招的数组,将需要的动作添加到数组当中。我这里添加了五个手部攻击的效果:

let attackList = [12, 13, 14, 15, 16]; //连招的循序
let attackCombo = false; //是否连招,接下一个攻击

我们还设置了attackCombo设置当前是否可以连招的变量,这个变量state.skills值不为0时,将变为true。定时器每一次更新的时候,将判断attackCombo是否为true,在为true的状态下,将执行连招的下一个动作。否则,将停止连招。

//attackIndex 等于0,当前不处于攻击状态  不等于,当前处于攻击状态
if(state.skills === 0){state.skills++;gui["action" + attackList[state.skills-1]]();attackInterval = setInterval(function () {if(attackCombo){//如果设置了连招,上一个攻击动作完成后,进行下一个攻击动作state.skills++;//如果整套攻击动作已经执行完成,则清除定时器if(state.skills-1 >= attackList.length){closeAttack();return;}//进行下一个动作gui["action" + attackList[state.skills-1]]();attackCombo = false;}else{closeAttack();}}, naruto.animations[attackList[state.skills-1]].duration*1000);
}
else{attackCombo = true;
}

在关闭掉攻击动画的函数内,我们首先将state.skills设置为0,然后恢复到移动或者站立动画,最后清除掉定时器:

//关闭攻击状态
function closeAttack() {state.skills = 0;//根据状态设置是移动状态还是站立状态state.move ? gui["action" + 3]() :gui["action" + 24](); //回到站立状态clearInterval(attackInterval);
}

通过很简单的一些代码,我们就实现了一个复杂的连招效果。是不是很有成就感,这就是在最前面看到的那个操作gif的效果的案例。最后,奉上案例查看页面:点击这里

16 Three.js 游戏操作案例相关推荐

  1. MV* 框架 与 DOM操作为主 JS库 的案例对比

    最近分别使用 Zepto 和 Avalon框架写了个 SPA项目,贴出来讨论下 JS DOM操作为主 JS库 与 MV* 框架的对比 案例(MV* 框架 与 DOM操作 JS库 实例对比) 购物车页面 ...

  2. js学习-操作元素案例-分时显示不同图片不同问候语

    js学习-操作元素案例-分时显示不同图片不同问候语 // 操作元素案例 // 分时显示不同图片,不同问候语// 上午7-11 中午12-13 下午14-18 晚上18-22![请添加图片描述](htt ...

  3. (附源码)node.js游戏网站 毕业设计 031726

    游戏网站的设计 摘 要 基于网络游戏的蓬勃发展,游戏网站发挥着吸引玩家和提高玩家之间的互动性的重要作用,因而,建设了一个以游戏为中心的游戏官网. 该游戏提供了一个大型的玩家交流互动平台,包括用户管理. ...

  4. (附源码)node.js 游戏网站 毕业设计 031726

    游戏网站的设计 摘 要 基于网络游戏的蓬勃发展,游戏网站发挥着吸引玩家和提高玩家之间的互动性的重要作用,因而,建设了一个以游戏为中心的游戏官网. 该游戏提供了一个大型的玩家交流互动平台,包括用户管理. ...

  5. 微信小游戏入门案例——拼图游戏

    微信小游戏入门案例--拼图游戏 涉及内容:canvas组件.小程序界面绘图API 目录结构: pages\game\game.js // pages/game/game.js // 方块的初始位置 v ...

  6. Three.JS游戏开发入门

    就在不久前,创建和部署游戏的唯一方法是选择像 Unity 或 Unreal 这样的游戏引擎,学习语言,然后打包游戏并将其部署到你选择的平台上. 试图通过浏览器向用户提供游戏的想法似乎是一项不可能完成的 ...

  7. 超全的auto.js基础操作,目前是autoX.js的控制方式。2023年7月9日更新!(第2/4章)

    02_auto.js基础操作2/4 文章目录 02_auto.js基础操作2/4 复杂界面 待办事项 登录界面 界面模板 用户调查 一个小测试 画布 函数图像简单版 函数图像高级版 脚本引擎 停止所有 ...

  8. AngularJS基于MVC的复杂操作案例

    AngularJS基于MVC的复杂操作案例 <!DOCTYPE html> <html><head><meta charset="UTF-8&quo ...

  9. html中js添加或删除activex,JS:操作样式表2 :用JS实现添加和删除一个类名的功能(addClass()和removeClass())...

    var box = document.getElementById("box"); box.id = "pox"; 将id = "box", ...

  10. html5 js选择器,使用HTML5的JS选择器操作页面中的元素

    文件命名为:querySelector.html,可在Chrome浏览器中预览效果. 1 2 3 4 5 使用HTML5的JS选择器操作页面中的元素 6 7 8 9 10 兴趣爱好: 11 12   ...

最新文章

  1. retinaface查看样本
  2. C#中使用MD5对用户密码加密与解密
  3. jquery sleep函数
  4. java 设置两个方法互斥_分享两个操作Java枚举的实用方法
  5. python 计算小于某个数_python 列表寻找满足某个条件的开始索引和结束索引、区间范围...
  6. python绘制国际象棋_python使用turtle绘制国际象棋棋盘
  7. [译] 使用 iPhone X 与 Maya 实现快速面部捕捉
  8. 简单区块链Python实现
  9. MVC项目开发中那些用到的知识点(Ajax.BeginForm)
  10. Unity3D 本地数据持久化记录存储
  11. 安装 android 模拟器
  12. 3dvary灯光材质为什么不亮_3dmax灯光教程灯光打出来太假?不真实?杂点?曝光?原因都在这儿呢...
  13. Amoeba for Aladdin
  14. 作为程序员的我,常用的工具软件有这些
  15. python怎么编写流氓软件_Python恶意软件分析入门
  16. 恒流源电路的几种实现方式(已通过Proteus仿真验证)
  17. 新生研讨课后感想——
  18. 什么是知识图谱?有哪些典型应用?终于有人讲明白了
  19. mysql官网下载后解压是文件夹_mysql解压文件安装
  20. leetcode系列-617.合并二叉树

热门文章

  1. 2010年3月刷Q币最新代码
  2. Plist图集批量拆解工具大全
  3. js如何获取计算机当前时间,js获取当前系统时间实例代码
  4. 克转换成千克怎么算python_斤公斤千克的换算(克和公斤怎么转换)
  5. Excel一键给操作内容添加批注作者
  6. C# winform excel根据当前选中内容,自动插入/编辑批注
  7. 0基础尝试实现UnityURP渲染管线的AdditionalLit(点光源,聚光灯)非平行光的光照模型代码
  8. 陈省身文集53——大范围微分几何若干新观点
  9. 最炫python表白代码_python炫酷烟花表白源代码
  10. JavaScript分号使用指南