草庐IT

Three.js 进阶之旅:物理效果-碰撞和声音 💥

dragonir 2023-03-28 原文

声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。

摘要

本文内容主要汇总如何在 Three.js 创建的 3D 世界中添加物理效果,使其更加真实。所谓物理效果指的是对象会有重力,它们可以相互碰撞,施加力之后可以移动,而且通过铰链和滑块还可以在移动过程中在对象上施加约束。 通过本文的阅读,你将学习到如何使用 Cannon.jsThree.js 中创建一个 3D 物理世界,并在物理世界更新对象、联系材质、施加外力、处理多个物体中添加物体之间的碰撞效果,通过检测碰撞激烈程度来添加撞击声音等。

效果

本文最终将实现如下所示的效果,点击 DAT.GUI 中创建立方体 ? 和球体 ? 的按钮,对应的物体将在拥有重力的三维世界中坠落,物体与地面及物体与物体之间发生碰撞时可以产生与碰撞强度匹配的撞击音频 ?,点击重置按钮,创建的物体将被清除。

打开以下链接,在线预览效果,大屏访问效果更佳。

本专栏系列代码托管在 Github 仓库【threejs-odessey】后续所有目录也都将在此仓库中更新

? 代码仓库地址:git@github.com:dragonir/threejs-odessey.git

原理

专栏之前的原理和示例学习中,我们已经可以使用光照、阴影、Raycaster 等特性生成一些简单的物理效果,但是如果需要实现像物体张力、摩擦力、拉伸、反弹等物理效果时,我们可以使用一些专业的物理特性开源库来实现。

为了实现物理效果,我们将在 Three.js 中创建一个物理世界,它纯粹是理论性质的,我们无法直接看到它,但是在其中,三维物体将产生掉落、碰撞、摩擦、滑动等物理特性。具体原理是当我们在 Three.js 中创建一个网格模型时,同时会将其添加到物理世界中,在每一帧渲染任何内容之前我们会告诉物理世界如何自行更新,然后我们将获取物理世界中更新的位移和旋转坐标数据,将其应用到 Three.js 三维网格中。

已经有很多功能完备的物理特性库,我们就没必要重复造轮子了。物理特性库可以分为 2D 库和 3D 库,虽然我们是使用 Three.js 开发三维功能,但是有些 2D库 在三维世界中同样是适用的而且它们的性能会更好,如果我们需要开发的物理功能是碰撞类的,则可以使用 2D 库,比如Ouigo Let's play就是一个使用 2D 库开发的优秀示例。下面是一些常用的物理特性库。

对于 3D 物理库,主要有以下三个:

对于 2D 物理库,有很多,下面列出了比较流行的几个:

本文内容及示例将使用 Cannon.js 库,因为它更容易理解和使用,对于其他库,使用原理基本上是一样的,大家感兴趣的话可以自行尝试。

Cannon.js

Cannon.js 是一个 3D 物理引擎,通过为物体赋予真实的物理属性的方式来计算运动、旋转和碰撞检测。Cannon.js 相较于其他常见的物理引擎来说,比较轻量级而且完全通过 JavaScript 来实现。主要有以下特性:

  • 刚体动力学
  • 离散碰撞检测
  • 接触、摩擦和恢复
  • 点到点约束、铰链约束、锁紧装置约束等
  • Gauss-Seidel 约束求解器与孤岛分割算法
  • 碰撞过滤
  • 刚体休眠
  • 实验性 SPH 流体支持
  • 各种形状和碰撞算法

Cannon-es

Cannon.js 库已经多年没有更新了,但是另一库 Cannon-es 克隆了原仓库并致力于长期更新维护新的仓库,可以像下面这样安装并使用,Cannon-es 用法和 Cannon.js 用法是完全一致的。

实现

? 本文示例及相关教程翻译并整理自 three.js journey 相关课程。

开始

安装并引入

npm install cannon --save
// 或
npm install --save cannon-es
import CANNON from 'cannon';
// 或
import * as CANNON from 'cannon-es';

初始化场景是一个平面 ? 和一个球体 ?,为了更好观察物理特性,已经开启了阴影效果。

我们可以使用 WebGL 创建一个无重力的太空场景,但是为了模拟地球环境 ? ,就需要添加重力,在 Cannon.js 中可以通过修改 gravity 属性值来实现,它是一个 Cannon.js Vec3 值,和 Three.js 中的 Vector3 一样,它包含 xyz 属性且拥有一个 set(...) 方法

world.gravity.set(0, -9.82, 0);

我们使用 -9.82 作为重力的 y 值,是因为它是地球的重力系数,如果你想让物理坠落的更慢或者想创建一个火星重力环境 ? ,就可以把它改为其他数值。

基础

世界

首先,我们需要创建一个 Cannon.js 世界:

const world = new CANNON.World();

对象

我们在场景中已经创建了一个球体,现在来在 Cannon.js 世界中创建一个球体。为了实现它,我们首先必须创建一个刚体Body,刚体是一种简单的对象,可以坠落和其他刚体产生碰撞。创建刚提前,我们首先需要决定刚体的形状,有很多形状可选,比如 BoxCylinderPlane 等,我们创建一个和 Three.js 中球体相同半径的球状刚体

const sphereShape = new CANNON.Sphere(0.5);

然后,创建一个初始化 mass 质量及 position 位置的 Body 刚体:

const sphereBody = new CANNON.Body({
  mass: 1,
  position: new CANNON.Vec3(0, 3, 0),
  shape: sphereShape
});

最后,我们通过 addBody(...) 方法将创建的刚体添加到世界中:

world.addBody(sphereBody);

此时查看页面可以看到没有任何效果,我们还需要更新 Cannon.js 世界和 Three.js 球体坐标。为更新物理世界world,我们必须使用时间步长step(...)方法。

更新

现在需要实现更新 Cannon.js 世界和 Three.js 场景。此时我们需要使用 step(...) 方法,为了使其生效,必须提供一个固定时间步长、自上次调用函数以来经过的时间、以及每个函数调用可执行的最大固定步骤数作为参数。

step(dt, [timeSinceLastCalled], [maxSubSteps=10])
  • dt:固定时间戳,要使用的固定时间步长
  • [timeSinceLastCalled]:自上次调用函数以来经过的时间
  • [maxSubSteps=10]:每个函数调用可执行的最大固定步骤数

? 关于时间步长原理,可查看此文章

在动画函数中,我们希望以 60fps 运行,因此将第一个参数设置为 1/60,这个设置在更高或更低帧率的情况下都能以相同速度运行;对于第二个参数,我们需要计算自上一帧以来经过了多少时间,通过将前一帧的 elapsedTime 减去当前 elapsedTime 来获得,不要直接使用 Clock 类中的 getDelta() 方法,因为无法得到预期的结果还会弄乱内部逻辑;第三个迭代参数,可以随便设置一个值,运行体验是否丝滑并不重要。

const clock = new THREE.Clock();
let oldElapsedTime = 0;

const tick = () => {
  const elapsedTime = clock.getElapsedTime();
  const deltaTime = elapsedTime - oldElapsedTime;
  oldElapsedTime = elapsedTime;
  //更新物理世界
  world.step(1/60,deltaTime,3)
  controls.update()
  renderer.render(scene, camera)
  window.requestAnimationFrame(tick)
}

此时查看页面,看起来仍然没有变化,但实际上物理世界中的球体刚体 sphereBody 正在不断下坠,可以通过如下的打印日志 ? 可以观察到。

console.log(sphereBody.position.y);

现在我们需要使用物理世界的 sphereBody 刚体坐标来更新 Three.js 中的球体,可以使用如下两种方法实现该功能:

// 方法一
sphere.position.x = sphereBody.position.x;
sphere.position.y = sphereBody.position.y;
sphere.position.z = sphereBody.position.z;
// 方法二
sphere.position.copy(sphereBody.position);

? copy方法在 Vector2、Vector3、Euler、Quaternion 甚至 Material、Object3D、Geometry 等类中都是可用的。

此时就能看到小球 ? 坠落的效果,但是它直接穿过了地面,因为现在仅在 Three.js 场景中添加了地面,而没有在 Cannon.js 物理世界中创建地面的刚体。

现在我们使用平面形状 Plane 来创建地面刚体,地面不应该受到物理世界重力的影响而下沉,它应该是保持静止不动的,我们可以通过如下方法将 mass 设置为 0 来实现:

const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.addShape(floorShape);
world.addBody(floorBody);

此时你会发现小球 ? 坠落的方向变了,并不是我们预期的结果,它应该落到地面上。因为物理世界中添加的平面是面向相机 ? 的,我们需要像在 Three.js 中旋转平面一样对它进行旋转。在 Cannon.js 中,我们只能使用四元数 Quaternion 来对刚体进行旋转,可以通过 setFromAxisAngle(...) 方法:

  • 第一个参数是旋转轴
  • 第二个参数是角度
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(- 1, 0, 0), Math.PI * 0.5);

现在可以看到小球 ? 从高处下落并且停在地面上,因为地面是静止不动的,因此我们不需要使用 Cannon.js 中的地面来更新 Three.js 中的地面。

联系材质

从上图可以观察到,小球 ? 坠落到地面后并没有反复弹跳,我们可以通过修改设置 Cannon.js 中的 MaterialContactMaterial添加摩擦和弹跳效果。

一个 Material 仅仅是一个类,你可以用它创建一种材质并命名后将它关联到 Body 刚体上,对于场景中所有的材质,都可以通过此方法进行创建。比如,假设世界中的所有物体都是塑料材质的,此时你只需创建一种材质即可,可以将它命名为 defaultplastic;如果场景中地面和小球是不同材质的,就需要根据它们的类型创建多种材质。下面我们为示例中的两类物体分别创建名为混凝土 concrete 和 塑料 plastic 的材质:

const concreteMaterial = new CANNON.Material('concrete');
const plasticMaterial = new CANNON.Material('plastic');

接下来,我们使用创建的两种材质来创建联系材质 ContactMaterial,它是两种材质的组合,包含对象碰撞时的属性。然后使用 addContactMaterial(...) 方法将它添加到世界中:

const concretePlasticContactMaterial = new CANNON.ContactMaterial(
  concreteMaterial,
  plasticMaterial,
  {
    friction: 0.1,
    restitution: 0.7
  }
)
world.addContactMaterial(concretePlasticContactMaterial)
ContactMaterial (material1, material2 , [options])
  • 前两个参数是材质
  • 第三个参数是碰撞属性对象,包含摩擦系数和恢复系数,两者的默认值均为 0.3

接着我们将创建好的 Material 应用到 Body 上,可以在实例化主体时直接传递材质,也可以在实例化之后使用材质属性传递材质。现在可以看到小球 ? 下落后在停止之前会返回弹跳多次:

const sphereBody = new CANNON.Body({
  material: plasticMaterial
})
// 或者
const floorBody = new CANNON.Body()
floorBody.material = concreteMaterial

场景中一般会有多种材质 Materials 的物体,为每种两两组合创建 ContactMaterial 会费时费解,为了简化这一操作,我们来使用一种默认材质来替换创建联系材质时的两种材质,并将它应用到所有刚体上:

const defaultMaterial = new CANNON.Material('default');
const defaultContactMaterial = new CANNON.ContactMaterial(
  defaultMaterial,
  defaultMaterial,
  {
    friction: 0.1,
    restitution: 0.7
  }
);
world.addContactMaterial(defaultContactMaterial)l
sphereBody.material = defaultMaterial;
floorBody.material = defaultMaterial;

可以观察到效果是相同的。或者我们直接设置世界的默认联系材质defaultContactMaterial 属性,然后移除 sphereBodyfloorBodymaterial 属性,这样世界中的所有材质就都是相同的默认材质

world.defaultContactMaterial = defaultContactMaterial;

施加外力

对一个刚体 Body 有以下几种施加外力的方法:

  • applyForce(force, worldPoint):从空间中的一个特殊点对刚体施加力(不一定在刚体的表面),比如就像风推动所有物体一样,或微弱但突然的力推向多米诺骨牌,或者像强烈且突然的力把愤怒的小鸟推向城堡一样。
    • force:力的大小 Vec3
    • worldPoint:施加力的世界点 Vec3
  • applyImpulse:类似于 applyForce,但它不是因为增加导致加速度改变,而是直接作用于加速度。
  • applyLocalForce(force, localPoint):与 applyForce 相同,但是坐标系是刚体的局部坐标,即 (0, 0, 0) 将是刚体的中点,从物体的内部施力。
    • force:要应用的力向量 Vec3
    • localPoint:刚体中中要施加力的局部点 Vec3
  • applyLocalImpulse:与 applyImpulse 相同,但是坐标系是刚体的局部坐标,即从物体的内部施力。

现在我们使用 applyLocalForce(...) 来为小球刚体 sphereBody 开始时施加一个小冲击力:

sphereBody.applyLocalForce(new CANNON.Vec3(150, 0, 0), new CANNON.Vec3(0, 0, 0));

可以看到小球 ? 向右弹跳并滚动。

现在我们使用 applyForce(...) 方法来施加一点风力 ? ,因为风是永久性的,因此在更新 World 之前,我们需要将这种力施加到每一帧。要正确应用此力,受力点应该是小球的位置 sphereBody.position

const tick = () => {
  // ...
  sphereBody.applyForce(new CANNON.Vec3(- 0.5, 0, 0), sphereBody.position)
  world.step(1 / 60, deltaTime, 3)
  // ...
}

处理多个物体

对一个或两个物体添加物理效果比较简单,但是为很多个物体都按上述方法添加就会非常复杂,我们需要添加一个自动化处理方法

自动处理函数

首先,移除或注释掉 Cannon.js 世界和 Three.js 中的球体,还有动画函数 tick() 中球体的设置,然后创建一个 createSphere 方法来生成小球:

const createSphere = (radius, position) => {
  // Three.js mesh
  const mesh = new THREE.Mesh(
    new THREE.SphereGeometry(radius, 20, 20),
    new THREE.MeshStandardMaterial({
      metalness: 0.4,
      roughness: 0.4,
      color: 0xfffc00
    })
  );
  mesh.castShadow = true;
  mesh.position.copy(position);
  scene.add(mesh);

  // Cannon.js body
  const shape = new CANNON.Sphere(radius);
  const body = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: shape,
    material: defaultMaterial
  });
  body.position.copy(position);
  world.addBody(body);
}

接着使用如下方法来创建一个小球 ? ,其中 position 参数不必是 Three.js 中的 Vector3 或者 Cannon.js 中的 Vec3,只需使用 x, y ,z 即可:

createSphere(0.5, { x: 0, y: 3, z: 0 });

可以看到地面顶部的创建的小球,但是由于我们移除了将 Cannon.js 世界中小球的 position 拷贝到 Three.js 中的方法,现在的小球暂时没有物理下坠效果

使用一个对象数组

为了使批量创建的小球得到更新,我们使用一个数组 objectsToUpdate 在创建函数中保存它们:

const objectsToUpdate = [];
const createSphere = (radius, position) => {
  // ...
  objectsToUpdate.push({
    mesh,
    body
  });
}

然后在动画方法 tick() 中批量将小球的 body.position 拷贝到 mesh.position

const tick = () => {
  // ...
  for (const object of objectsToUpdate) {
    object.mesh.position.copy(object.body.position);
  }
}

此时,批量创建的小球 ? 也有物理效果了。

添加Dat.GUI

为了方便调试,我们给页面按如下方式添加 Dat.GUI 调试工具,并添加一个 createSphere 来在场景中创建多个小球:

const gui = new dat.GUI();
const debugObject = {};
debugObject.createSphere = () => {
// 使用随机数创建随机大小和位置的小球
createSphere(
  Math.random() * 0.5,
  {
    x: (Math.random() - 0.5) * 3,
    y: 3,
    z: (Math.random() - 0.5) * 3
  }
)
}
gui.add(debugObject, 'createSphere');

优化

因为 Three.js 网格 Meshgeometrymaterial 都是一样的,我们应该将其移出 createSphere 方法,由于我们使用 radius 来创建几何体的,为了兼容之前的方法,我们可以按如下方式将 SphereGeometry 半径设置为 1,并使用 scale 来调整几何体的大小,得到的结果和上面是一致的,但是性能得以提升

const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const sphereMaterial = new THREE.MeshStandardMaterial({
  metalness: 0.4,
  roughness: 0.4,
  color: 0xfffc00
});

const createSphere = (radius, position) => {
  const mesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
  mesh.castShadow = true;
  mesh.scale.set(radius, radius, radius);
  mesh.position.copy(position);
  scene.add(mesh)
// ...
}

添加立方体

现在我们使用相同的流程添加一个创建立方体 ? 的方法 createBox,其中传入的参数将是 widthheightdepthposition。需要注意的是,Cannon.js 中创建BoxThree.js 创建 Box 不同,在 Three.js 中,创建几何体BoxBufferGeometry 只需要直接提供立方体的宽高深就行,但是在Cannon.js中,它是根据立方体对角线距离的一半来计算生成形状,因此其宽高深必须乘以0.5

// 创建立方体
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshStandardMaterial({
  metalness: 0.4,
  roughness: 0.4,
  color: 0x0091ff
})
const createBox = (width, height, depth, position) => {
  // Three.js 网格
  const mesh = new THREE.Mesh(boxGeometry, boxMaterial);
  mesh.scale.set(width, height, depth);
  mesh.castShadow = true;
  mesh.position.copy(position);
  scene.add(mesh);
  // Cannon.js 刚体
  const shape = new CANNON.Box(new CANNON.Vec3(width * 0.5, height * 0.5, depth * 0.5))
  const body = new CANNON.Body({
    mass: 1,
    position: new CANNON.Vec3(0, 3, 0),
    shape: shape,
    material: defaultMaterial
  })
  body.position.copy(position);
  world.addBody(body);
  // 保存在更新对象数组中
  objectsToUpdate.push({ mesh, body });
}

createBox(1, 1.5, 2, { x: 0, y: 3, z: 0 });

// 添加到DAT.GUI
debugObject.createBox = () => {
  createBox(
    Math.random(),
    Math.random(),
    Math.random(),
    {
      x: (Math.random() - 0.5) * 3,
      y: 3,
      z: (Math.random() - 0.5) * 3
    }
  )
}
gui.add(debugObject, 'createBox');

先移除创建小球的方法,页面运行可以得到如下的结果:

现在可以创建随机的立方体了,但是看起来有点奇怪不太逼真是不是?因为立方体掉下来后没有翻转,原因是 Three.js 中的网格没有像 Cannon.js 中的刚体一样旋转,在球体的示例中我们没有发现是因为无论球体是否旋转都是和原来一样的,而在立方体中不一样。我们可以通过如下将刚体的 quaternion 属性拷贝到网格的 quaternion 属性来实现,就像之前拷贝位置属性 position 一样:

const tick = () => {
  // ...
  for (const object of objectsToUpdate) {
    object.mesh.position.copy(object.body.position);
    object.mesh.quaternion.copy(object.body.quaternion);
  }
  // ...
}

现在立方体 ? 坠落时的旋转也正常了。

性能优化

Broadphase

测试物体之间的碰撞时,一种方法是检测一个刚体与另外所有其他刚体之间的碰撞,虽然这一操作很容易实现,但是非常耗费性能。此时就需要 Broadphase,它会在测试之前对刚体进行粗略的分类,想象一下,两堆相距很远的立方体,为什么要用一堆立方体来测试另一堆立方体之间的碰撞关系能,它们相距很远,不会发生碰撞,因此就没必要测试来耗费性能。

Cannon.js 中共有 3Broadphase 算法:

  • NaiveBroadphase:测试每个刚体与其他所有刚体之间的碰撞,默认算法。
  • GridBroadphase: 使用四边形栅格覆盖 world,仅针对同一栅格或相邻栅格中的其他刚体进行碰撞测试。
  • SAPBroadphase:扫描剪枝算法,在多个步骤的任意轴上测试刚体。

NaiveBroadphase 是默认检测方法,但是推荐使用 SAPBroadphase 算法,虽然这种算法有时可能会产生检测不会发生碰撞的错误,但是它的检测速度非常快。通过如下方式,简单设置 world.broadphase 属性即可修改碰撞检测算法:

world.broadphase = new CANNON.SAPBroadphase(world);

Sleep ?

即使我们使用改进的 Broadphase 碰撞检测算法,有可能所有的刚体都会被检测,即使是那些不再发生移动的刚体。此时我们可以使用称为 Sleep 的特性,当刚体的速度逐渐变小不再发生移动,它就会进入睡眠状态,此时就不会对它进行碰撞检测,除非使用代码让其施加一个足够的力再次运动或有其它的刚体击中它。可以通过对 world 设置 allowSleep 属性为 true 来实现:

world.allowSleep = true;

你也可以使用 sleepSpeedLimitsleepTimeLimit 属性对睡眠速度和时间进行详细设置,但是一般不会改变默认值。

事件

可以对刚体的事件进行监听,比如你想在物体发生碰撞时播放呻吟或者在射击游戏中检测是否命中敌人等情况下是非常有用的。你可以在刚体上监听 colidesleepwakeup 等事件。

现在,我们来实现一下当场景中的小球 ? 和立方体 ? 互相之间发生碰撞时播放声音 ? 的功能。首先在 JavaScript 中创建音频,并添加一个方法来播放它。

? 有些浏览器比如 Chrome 默认会静音 ? 除非用户与页面发生交互,例如点击任意区域,所以不要担心首次加载时不播放声音的问题

const hitSound = new Audio('/sounds/hit.mp3');
const playHitSound = () => {
  // 播放时间重置为0,解决多次调用时声音间断问题
  hitSound.currentTime = 0
  hitSound.play()
}

然后在创建立方体方法 createBox 中调用:

const createBox = (width, height, depth, position) => {
  // ...
  body.addEventListener('collide', playHitSound);
  // ...
}

此时,当立方体 ? 撞击到地面或相互碰撞时可以听到撞击声音 ?,看起来似乎是正确的,但是当添加多个立方体时,我们会听到很多立方体之间相互撞击的声音是一样的,而现实中的声音应该是根据声音随着立方体之间的撞击程度而不同,撞击程度足够小的话就听不到声音。为了获取撞击的强度,我们需要获取撞击信息,可以通过如下给 playHitSound 方法添加参数 collision 的方式来获取撞击信息:

const playHitSound = (collision) => {
const impactStrength = collision.contact.getImpactVelocityAlongNormal();
  // 只有撞击强度足够大时才播放撞击音频
  if (impactStrength > 1.5) {
    // 为了更加真实,可以给音量添加一些随机性
    hitSound.volume = Math.random();
    hitSound.currentTime = 0;
    hitSound.play();
  }
}

然后在创建球体的方法 createSphere 中同样调用播放撞击音频方法:

const createSphere = (radius, position) => {
  // ...
  body.addEventListener('collide', playHitSound)
  // ...
}

移除物体

当页面上添加过多物体时,我们可以通过在 Dat.GUI 添加一个重置按钮来移除已添加的物体,通过遍历 objectsToUpdate 数组,将每个数组项对应的的 object.body 从 物理世界 world 中移除,将 object.meshThree.js 场景中移除,并清除 collide 碰撞事件的 eventListener

debugObject.reset = () => {
  for (const object of objectsToUpdate) {
    object.body.removeEventListener('collide', playHitSound);
    world.removeBody(object.body);
    scene.remove(object.mesh);
  }
}
gui.add(debugObject, 'reset');

总结

本文中主要包含的知识点包括:

  • Three.js 中添加物理效果基本原理
  • 常用 3D2D 物理物理引擎汇总
  • Cannon.jsCannon-es 安装与引用
  • 物理世界创建、对象更新、联系材质、施加外力、处理多个物体
  • 碰撞事件监听、音频添加
  • 性能优化、物理世界移除物体等

想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 ?

附录

参考

本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17121487.html

有关Three.js 进阶之旅:物理效果-碰撞和声音 💥的更多相关文章

  1. Ruby 元类 : why three when defined singleton methods? - 2

    让我们计算MRI范围内的类别:defcount_classesObjectSpace.count_objects[:T_CLASS]endk=count_classes用类方法定义类:classAdefself.foonilendend然后运行:putscount_classes-k#=>3请解释一下,为什么是三个? 最佳答案 查看MRI代码,每次你创建一个Class时,在Ruby中它是Class类型的对象,ruby会自动为这个新类创建“元类”类,这是另一个单例类型的Class对象。C函数调用(class.c)是:rb_define

  2. ruby-on-rails - Assets 管道损坏 : Not compiling on the fly css and js files - 2

    我开始了一个新的Rails3.2.5项目,Assets管道不再工作了。CSS和Javascript文件不再编译。这是尝试生成Assets时日志的输出:StartedGET"/assets/application.css?body=1"for127.0.0.1at2012-06-1623:59:11-0700Servedasset/application.css-200OK(0ms)[2012-06-1623:59:11]ERRORNoMethodError:undefinedmethod`each'fornil:NilClass/Users/greg/.rbenv/versions/1

  3. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  4. node.js - 如何在 Travis CI 上的一个项目中运行 Node.js 和 Ruby 测试 - 2

    我有一个包含多个组件的存储库,其中大部分是用JavaScript(Node.js)编写的,一个是用Ruby(RubyonRails)编写的。我想要一个.travis.yml文件来触发一个运行每个组件的所有测试的构建。根据thisTravisCIGoogleGroupthread,目前还没有官方支持。我的目录结构是这样的:.├──构建服务器├──核心├──扩展├──网络应用├──流浪文件├──package.json├──.travis.yml└──生成文件我希望能够运行特定版本的Ruby(2.2.2)和Node.js(0.12.2)。我已经有了一个make目标,所以maketest在每

  5. 玩以太坊链上项目的必备技能(初识智能合约语言-Solidity之旅一) - 2

    前面一篇关于智能合约翻译文讲到了,是一种计算机程序,既然是程序,那就可以使用程序语言去编写智能合约了。而若想玩区块链上的项目,大部分区块链项目都是开源的,能看得懂智能合约代码,或找出其中的漏洞,那么,学习Solidity这门高级的智能合约语言是有必要的,当然,这都得在公链``````以太坊上,毕竟国内的联盟链有些是不兼容Solidity。Solidity是一种面向对象的高级语言,用于实现智能合约。智能合约是管理以太坊状态下的账户行为的程序。Solidity是运行在以太坊(Ethereum)虚拟机(EVM)上,其语法受到了c++、python、javascript影响。Solidity是静态类型

  6. ruby - 在不同的文件中设置断点没有效果 - 2

    ruby调试器不会在我在与执行开始时不同的文件中设置的断点处停止。例如,考虑这两个文件,foo.rb:#foo.rbclassFoodefbarputs"baz"endend和main.rb:#main.rbrequire'./foo'Foo.new.bar我使用ruby-rdebug.\main.rb开始调试。现在,当我尝试使用b./foo.rb:4在另一个文件的特定行上设置断点时,我收到消息Setbreakpoint1atfoo.rb:4,但是当我cont时,程序执行到最后,调试器永远不会停止。但是,如果我在main.rb中的一行上打断,例如b./main.rb:3,或者一个方法,

  7. node.js - 从未编写过任何自动化测试,我应该如何开始行为驱动开发? - 2

    按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。多年来,我一直在使用多种语言进行编程,并且认为自己总体上相当擅长。但是,我从未编写过任何自动化测试:没有单元测试,没有TDD,没有BDD,什么都没有。我已经尝试开始为我的项目编写适当的测试套件。我可以看到在进行任何更改后能够自动测试项目中所有代码的理论值(value)。我可以看到像RSpec和Mocha这样的测试框架应该如何使设置和运行所述测试变得相当容易

  8. ruby-on-rails - 将 Angular JS 与 Rails 集成 - 2

    我需要一些指导来了解如何将Angular整合到rails中。选择Rails的原因:我喜欢他们偏执的做事方式。还有迁移,gem真的很酷。使用angular的原因:我正在研究和寻找最适合SPA的框架。Backbone似乎太抽象了。我不得不在Angular和Ember之间做出选择。我首先开始阅读Angular,它对我来说很有意义。所以我从来没有去读过关于ember的文章。使用Angular和Rails的原因:我研究并尝试使用小型框架,例如grape、slim(是的,我也使用php)。但我觉得需要坚持项目的长期范围。我个人喜欢用Rails的方式做事。这就是我需要帮助的地方,我在Rails4中有

  9. ruby-on-rails - Ruby表达式 '-'后留空格的效果 - 2

    今天我在我的Rails控制台中尝试了一些东西,这发生了,2.0.0p247:009>Date.today-29.days=>Fri,07Feb20142.0.0p247:010>Date.today-29.days=>Thu,09Jan2014我很困惑。我可以看到我缺少一些基本的东西。但这让我印象深刻!谁能解释为什么会这样? 最佳答案 实际发生的是这样的:Date.today(-29.days)#=>Fri,07Feb2014today有一个名为start的可选参数,默认为Date::ITALY。Anoptionalargument

  10. node.js - 如何比较图像并确定哪个内容更多? - 2

    目标:我想从动画GIF中抓取最佳帧并将其用作静态预览图像。我相信最好的帧是显示最多内容的帧-不一定是第一帧或最后一帧。以这张动图为例:--这是第一帧:--这是第28帧:很明显,第28帧很好地代表了整个GIF。我如何以编程方式确定一帧是否比另一帧具有更多像素/内容?如果您能向我指出任何想法、想法、包/模块或文章,我们将不胜感激。 最佳答案 实现此目的的一种直接方法是估计entropy每个图像的帧,并选择具有最大熵的帧。在信息论中,熵可以被认为是图像的“随机性”。单一颜色的图像是非常可预测的,分布越平坦,越随机。这与Arthur-R描述

随机推荐