Posted in

业务代码写累了,写个小游戏玩玩 – 掘金_AI阅读总结 — 包阅AI

包阅导读总结

1.

关键词:射击游戏、ThreeJS、编程开发、游戏要素、游戏效果

2.

总结:本文讲述作者工作累了开发射击小游戏,介绍了游戏玩法及基于 ThreeJS 开发的过程,包括创建场景、目标、更新位置、处理射击、检测击中、创建爆炸效果等要素,并整合实现最终效果,最后分享开发感触和代码上传情况。

3.

主要内容:

– 前言

– 工作劳逸结合,作者决定开发射击小游戏

– 游戏玩法

– 鼠标操作射击角度,按键控制移动和射击,击中有爆炸效果和分值

– 开发过程

– 创建射击场景

– 包括渲染器、场景、相机,监听窗口尺寸改变事件

– 加入地面和灯光

– 创建射击目标

– 随机生成位置和运动方向,避免重叠

– 更新目标位置,处理边界回拉

– 更新玩家位置

– 指针锁定控制器,监听按键事件

– 函数更新玩家位置

– 射击处理

– 按下空格绘制射出子弹效果

– 子弹有抛物线轨迹,受重力影响

– 更新子弹位置,超出距离消失

– 检测是否击中目标

– 计算子弹和目标距离,击中有爆炸效果,更新分值和重新创建目标

– 创建爆炸效果

– 粒子系统模拟,100ms 后移除

– 整合要素

– 初始化游戏环境

– 动画更新主循环,实现动态效果

– 最后

– 开发感触

– 代码上传码云

思维导图:

文章地址:https://juejin.cn/post/7390335741585883162

文章来源:juejin.cn

作者:去伪存真

发布时间:2024/7/11 11:48

语言:中文

总字数:3862字

预计阅读时间:16分钟

评分:84分

标签:游戏开发,Three.js,WebGL,射击游戏,前端技术


以下为原文内容

本内容来源于用户推荐转载,旨在分享知识与观点,如有侵权请联系删除 联系邮箱 media@ilingban.com

前言

工作要劳逸结合,人的天性是好逸恶劳。工作干累了,就给自己找点乐子,调节一下疲惫的身心状态。找什么乐子呢? 玩几把游戏,可是玩游戏有边际递减效应,前面玩的很开心,后面开心的劲头会越来越小。那这个时候该干什么呢?平常玩的游戏都是别人开发的,作为程序员,我们是有能力自己开发一个游戏的。何不自己开发一款小游戏,既能自娱自乐,又能提高编程技能,一举两得,何乐而不为。说干就干,笔者爱玩射击类的单机游戏,那就开发一个射击类的游戏吧。先演示一下最终的效果。

效果演示

游戏的玩法是:鼠标点击游戏界面之后,就能以第一人称视角通过鼠标自由地移动和旋转射击角度,按下ESC键,退出鼠标指针锁定功能。按上下左右四个方向键,可以向前后左右四个方向移动,按下空格键,会开火射击,击中目标时,会产生爆炸效果,增加射中分值。

轮播5.gif

射击类游戏的界面交互一般有五个要素:

  • 一是射击场景
  • 二是射击目标
  • 三是射击玩家
  • 四是射击轨迹
  • 五是击中目标后的爆炸效果

我们就依次开发这5个要素的功能,并把它们有机的整合在一起,构成一个完整的射击游戏。

创建射击场景

threeJS的基础入门知识可参照笔者以前写的这篇文章初探神秘的Three.js。一个典型的 Three.js 程序至少要包括渲染器(Renderer)场景(Scene)照相机(Camera), 以及向场景中加入的物体。依次创建渲染器,场景,照相机。并监听浏览器的窗口尺寸改变事件,在窗口大小变化时,调整摄像机的视角和渲染器的大小,确保场景在窗口大小变化时能够正确渲染。

const renderer = new THREE.WebGLRenderer();const scene = new THREE.Scene();const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, 0.1, 1000);adjustWindowSize();window.addEventListener("resize", adjustWindowSize);function adjustWindowSize() {  camera.aspect = window.innerWidth / window.innerHeight;  renderer.setSize(window.innerWidth, window.innerHeight);  document.body.appendChild(renderer.domElement);  renderer.render(scene, camera);}

向场景中加入地面和灯光

const floorGeometry = new THREE.PlaneGeometry(100, 100);const floorMaterial = new THREE.MeshBasicMaterial({ color: 0x808080, side: THREE.DoubleSide });const floor = new THREE.Mesh(floorGeometry, floorMaterial);floor.rotation.x = -Math.PI / 2;scene.add(floor);const light = new THREE.DirectionalLight(0xffffff, 1);light.position.set(5, 10, 7.5);scene.add(light);

创建射击目标

下面这段代码创建了一个目标物并将其添加到场景中,为每个目标物赋予随机颜色和随机运动方向,并确保目标物不会相互重叠在一起。

这句代码const target = new THREE.Mesh(targetGeometry, targetMaterial); 创建了一个网格,将几何形状和材质组合在一起,形成一个可渲染的对象。

while循环中的这行代码target.position.set(Math.random() * 100 - 50, 1, Math.random() * 100 - 50);将目标物随机放置在 X 和 Z 轴上,范围是 -50 到 50,Y 轴固定为 1。Three.JS采用的是右手坐标系,X 和 Z 轴方向如下图所示:

image.png

while循环中的for 循环遍历所有已存在的目标物,如果当前目标物与任何一个已存在的目标物距离小于 5,则认为位置无效,重新生成位置。这样做是为了防止生成的目标值堆叠在一起。

这行代码target.velocity = new THREE.Vector3((Math.random() - 0.5) * 0.1, 0, (Math.random() - 0.5) * 0.1); 为目标物设置一个随机速度,使其在 X 和 Z 轴上随机移动,速度范围在 -0.05 到 0.05 之间。

function createTarget() {  const targetGeometry = new THREE.BoxGeometry(2, 2, 2);  const targetMaterial = new THREE.MeshBasicMaterial({    color: new THREE.Color(Math.random(), Math.random(), Math.random()),  });  const target = new THREE.Mesh(targetGeometry, targetMaterial);  let validPosition = false;  while (!validPosition) {    target.position.set(Math.random() * 100 - 50, 1, Math.random() * 100 - 50);    validPosition = true;    for (let i = 0; i < targets.length; i++) {      const otherTarget = targets[i];      if (target.position.distanceTo(otherTarget.position) < 5) {                validPosition = false;        break;      }    }  }  target.velocity = new THREE.Vector3((Math.random() - 0.5) * 0.1, 0, (Math.random() - 0.5) * 0.1);  targets.push(target);  scene.add(target);}

更新射击目标位置

射击目标创建之后,需要在每个渲染帧更新目标物的位置,使它们在场景中延续原来的方向继续移动,当目标物移动到地面边界时,要进行回拉。具体步骤为:

  • 遍历每个目标物。
  • 根据其速度向量更新位置。
  • 检查是否超出地面边界,如果超出,则反转相应轴的速度方向,使其在边界处反弹。
function updateTargets() {  targets.forEach((target) => {    target.position.add(target.velocity);        if (target.position.x > 50 || target.position.x < -50) target.velocity.x = -target.velocity.x;    if (target.position.z > 50 || target.position.z < -50) target.velocity.z = -target.velocity.z;  });}

更新玩家位置

先创建一个指针锁定控制器,PointerLockControls 用于将鼠标指针锁定到浏览器窗口中,使得玩家可以使用鼠标控制相机的方向。当用户点击游戏页面时,调用 controls.lock() 方法将鼠标指针锁定。

const controls = new THREE.PointerLockControls(camera, document.body);document.addEventListener("click", () => controls.lock());

接着监听方向键和空格键的按下弹起事件:

const move = { forward: false, backward: false, left: false, right: false };const onKeyDown = function (event) {  switch (event.code) {    case "ArrowUp":      move.forward = true;      break;    case "ArrowLeft":      move.left = true;      break;    case "ArrowDown":      move.backward = true;      break;    case "ArrowRight":      move.right = true;      break;    case "Space":      shoot();      break;  }};const onKeyUp = function (event) {  switch (event.code) {    case "ArrowUp":      move.forward = false;      break;    case "ArrowLeft":      move.left = false;      break;    case "ArrowDown":      move.backward = false;      break;    case "ArrowRight":      move.right = false;      break;  }};document.addEventListener("keydown", onKeyDown);document.addEventListener("keyup", onKeyUp);

updatePlayer 函数用于更新玩家位置。内部创建了一个 direction 向量,用于存储玩家移动的方向和距离。 根据 move 对象的标记值,调整 direction 向量的值。使用 controls.moveRightcontrols.moveForward 方法将玩家移动到新的位置。

function updatePlayer() {  const speed = 0.1;  const direction = new THREE.Vector3();  if (move.forward) direction.z += speed;  if (move.backward) direction.z -= speed;  if (move.left) direction.x -= speed;  if (move.right) direction.x += speed;  controls.moveRight(direction.x);  controls.moveForward(direction.z);}

串起来就是通过 PointerLockControls 实现用鼠标控制玩家在 3D 场景中的移动。通过监听键盘按键事件,标记玩家的移动方向,并在每一帧中根据玩家的移动方向更新玩家的位置(这个方法会放在requestAnimationFrame中执行),这样就使得玩家可以使用鼠标和方向键在场景中移动,使用空格键进行射击。

射击处理

当按下空格键时,要绘制射出子弹的效果。子弹要冒火光,飞行轨迹看起来像抛物线。具体实现是:

  1. 创建一个小球形状的子弹 (SphereGeometry)。设置子弹的材质为黄色 (MeshBasicMaterial)。
  2. 根据当前相机的位置设置子弹的位置 (position.copy(controls.getObject().position)),即从玩家位置发射。
  3. 设置子弹的初始速度 (velocity) 和方向。applyQuaternion(camera.quaternion) 将速度方向旋转到相机的方向。
  4. 给子弹添加重力 (gravity),使子弹在飞行过程中受到重力影响。
  5. 将子弹添加到 bullets 数组中,并添加到场景中。之所以需要一个bullets 数组进行存储,是因为发射出去的子弹并不是仅有一颗,而且后续在每个渲染帧,要持续更新。所以在添加到场景之后,要将子弹的状态保存起来。
function shoot() {  const bulletGeometry = new THREE.SphereGeometry(0.1, 8, 8);  const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });  const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);  bullet.position.copy(controls.getObject().position);  bullet.velocity = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);  bullet.gravity = new THREE.Vector3(0, -0.001, 0);   bullets.push(bullet);  scene.add(bullet);}

更新子弹位置

子弹射出去之后,子弹的位置要持续更新。当子弹超出一定距离时,让子弹消失在视野中。具体实现步骤为:

  1. 遍历所有子弹,更新子弹的位置。
  2. 将重力作用添加到子弹的速度上 (bullet.velocity.add(bullet.gravity))。
  3. 更新子弹的位置 (bullet.position.add(bullet.velocity))。
  4. 如果子弹的位置超过一定距离(例如 100),则从场景中移除子弹,并从 bullets 数组中删除。
  5. 使用 filter 方法过滤掉超过距离的子弹。
function updateBullets() {  bullets = bullets.filter((bullet) => {    bullet.velocity.add(bullet.gravity);     bullet.position.add(bullet.velocity);    if (bullet.position.length() >= 100) {      scene.remove(bullet);      return false;    }    return true;  });}

检测是否击中目标

子弹射出之后,要比较子弹和射击目标的距离,判断是否击中目标。击中目标后,要有爆炸效果,并且需要移除子弹和击中目标,重新创建新的射击目标,并更新射击分值。实现步骤为:

  1. 遍历所有目标物 (targets) 和子弹 (bullets)。
  2. 计算目标物与子弹之间的距离 (distanceTo)。
  3. 如果距离小于1,即认为子弹击中了目标物。执行击中目标逻辑。
function checkHit() {  targets.forEach((target, tIndex) => {    bullets.forEach((bullet, bIndex) => {      const distance = target.position.distanceTo(bullet.position);      if (distance < 1) {        createExplosion(target.position);        scene.remove(target);        scene.remove(bullet);        targets.splice(tIndex, 1);        bullets.splice(bIndex, 1);        score += 10;        scoreElement.innerText = `分数: ${score}`;        createTarget();       }    });  });}

创建爆炸效果

目标被击中后,要产生爆炸效果,展示100ms后,移除爆炸元素。爆炸效果的实现步骤是:

  1. 通过一个粒子系统 (PointsMaterial) 来模拟爆炸效果,设置粒子的材质、纹理,大小、颜色、透明度等。
  2. 创建一个 BufferGeometry 来存储粒子的位置。 随机生成 100 个粒子的位置,创建一个 THREE.Points 对象,将随机生成的粒子其添加到场景中。
  3. 使用 setTimeout 在 100 毫秒后将爆炸元素从场景中移除。
const explosionTexture = new THREE.TextureLoader().load("./expose.jpg");function createExplosion(position) {  const explosionMaterial = new THREE.PointsMaterial({    size: 1,    map: explosionTexture,    blending: THREE.AdditiveBlending,    depthWrite: false,    transparent: true,    color: 0xff4500,  });  const explosionGeometry = new THREE.BufferGeometry();  const vertices = [];  for (let i = 0; i < 100; i++) {    const particle = new THREE.Vector3();    particle.x = position.x + (Math.random() - 0.5) * 5;    particle.y = position.y + (Math.random() - 0.5) * 5;    particle.z = position.z + (Math.random() - 0.5) * 5;    vertices.push(particle.x, particle.y, particle.z);  }  explosionGeometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));  const explosion = new THREE.Points(explosionGeometry, explosionMaterial);  scene.add(explosion);  setTimeout(() => {    scene.remove(explosion);  }, 100);}

这里要说明一下使用 THREE.TextureLoader 预加载爆炸效果的纹理图像,如果不进行预加载,首次击中目标时没有爆炸效果。

整合要素

前面已经实现了射击游戏的各个要素,现在需要对各个要素进行有序的整合。

  • 第一步是初始化游戏环境。创建游戏场景,向游戏场景中添加地面,灯光,射击目标,玩家,监听方向键和空格键的按下弹起事件,当事件发生时,执行相应的行为。
  • 第二步进行动画更新的主循环,不断地更新场景中的各个对象(玩家、子弹、目标物),并进行击中检测,然后将更新后的场景渲染出来,从而实现动态的游戏效果。
init();function init() {  for (let i = 0; i < 20; i++) {    createTarget();  }  animate();}function animate() {  requestAnimationFrame(animate);  updatePlayer();  updateBullets();  updateTargets();  checkHit();  renderer.render(scene, camera);}

到此,就实现了文章开头展示的射击游戏效果 。

最后

开发完这个游戏之后,有两点感触,第一点感觉使用ThreeJS开发游戏的要点是各种UI要素的生成,怎样才能生成酷炫一些的展示元素,这可能需要进行视觉效果建模(本文的UI效果都是从网上拼凑的)。目前感觉判断逻辑并不是很复杂(也可能很复杂,我还没深入到那个层面)。第二点是我玩自己开发的游戏,也很上头。尤其是击中目标的那一刻产生的爆炸效果,很让人沉浸。因为我平时很少玩游戏,所以玩游戏的快乐阈值很低。如果你经常玩王者荣耀这种制作精良的游戏,肯定会对文中这种小儿科的游戏嗤之以鼻,身体不会分泌多巴胺。不过咱的快乐感不全来自玩游戏时分泌的多巴胺,还有完成游戏开发之后身体分泌的内啡肽(成功开发了一款小游戏带来的成就感和充实感)。废话说完了,文中的代码已经上传到码云,如果你对整体串联流程还不太懂的话,可以点击这里下载查看。