
声明:本文涉及图文和模型素材仅用于个人学习、研究和欣赏,请勿二次修改、非法传播、转载、出版、商用、及进行其他获利行为。
本文在专栏上一篇内容《Three.js 进阶之旅:物理效果-碰撞和声音》的基础上,将使用新的技术栈 React Three Fiber 和 Cannon.js 来实现一个具有物理特性的小游戏,通过本文的阅读,你将学习到的知识点包括:了解什么是 React Three Fiber 及它的相关生态、使用 React Three Fiber 搭建基础三维场景、如何使用新技术栈给场景中对象的添加物理特性等,最后利用上述知识点,将开发一个简单的乒乓球小游戏。
在正式学习之前,我们先来看看本文示例最终实现效果:页面主体内容是一个手握乒乓球拍的模型和一个乒乓球 ?,对球拍像现实生活中一样进行颠球施力操作,乒乓球可以在球拍上弹起,乒乓球弹起的高度随着施加在球拍上的力的大小的变化而变化,球拍中央显示的是连续颠球次数 5️⃣,当乒乓球从球拍掉落时一局游戏结束,球拍上的数字归零 0️⃣ 。快来试试你一次可以颠多少个球吧 ?。

打开以下链接,在线预览效果,大屏访问效果更佳。
?? 在线预览地址:https://dragonir.github.io/physics-pingpong/本专栏系列代码托管在 Github 仓库【threejs-odessey】,后续所有目录也都将在此仓库中更新。
React Three Fiber 是一个基于 Three.js 的 React 渲染器,简称 R3F。它像是一个配置器,把 Three.js 的对象映射为 R3F 中的组件。以下是一些相关链接:

Three.js 的处理变得更加轻松,并使代码库更加整洁。这些组件对状态变化做出反应,具有开箱即用的交互性。Three.js 中所有内容都能在这里运行。它不针对特定的 Three.js 版本,也不需要更新以修改,添加或删除上游功能。Three.js 和 GPU 相仿。组件参与 React 之外的 render loop 时,没有任何额外开销。写 React Three Fiber 比较繁琐,我们可以写成 R3F 或简称为 Fiber。让我们从现在开始使用 R3F 吧。
R3F 有充满活力的生态系统,包括各种库、辅助工具以及抽象方法:
@react-three/drei – 有用的辅助工具,自身就有丰富的生态@react-three/gltfjsx – 将 GLTFs 转换为 JSX 组件@react-three/postprocessing – 后期处理效果@react-three/test-renderer – 用于在 Node 中进行单元测试@react-three/flex – react-three-fiber 的 flex 盒子布局@react-three/xr – VR/AR 控制器和事件@react-three/csg – 构造实体几何@react-three/rapier – 使用 Rapier 的 3D 物理引擎@react-three/cannon – 使用 Cannon 的 3D 物理引擎@react-three/p2 – 使用 P2 的 2D 物理引擎@react-three/a11y – 可访问工具@react-three/gpu-pathtracer – 真实的路径追踪create-r3f-app next – nextjs 启动器lamina – 基于 shader materials 的图层zustand – 基于 flux 的状态管理jotai – 基于 atoms 的状态管理valtio – 基于 proxy 的状态管理react-spring – 一个 spring-physics-based 的动画库framer-motion-3d – framer motion,一个很受欢迎的动画库use-gesture – 鼠标/触摸手势leva – 创建 GUI 控制器maath – 数学辅助工具miniplex – ECS 实体管理系统composer-suite – 合成着色器、粒子、特效和游戏机制、npm install three @react-three/fiber
在一个新建的 React 项目中,我们通过以下的步骤使用 R3F 来创建第一个场景。
首先,我们从 @react-three/fiber 引入 Canvas 元素,将其放到 React 树中:
import ReactDOM from 'react-dom'
import { Canvas } from '@react-three/fiber'
function App() {
return (
<div id="canvas-container">
<Canvas />
</div>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
Canvas 组件在幕后做了一些重要的初始化工作:
Scene 和一个相机 Camera,它们都是渲染所需的基本模块。
?Canvas 大小响应式自适应于父节点,我们可以通过改变父节点的宽度和高度来控制渲染场景的尺寸大小。
为了真正能够在场景中看到一些物体,现在我们添加一个小写的 <mesh /> 元素,它直接等效于 new THREE.Mesh()。
<Canvas>
<mesh />
?可以看到我们没有特地去额外引入mesh组件,我们不需要引入任何元素,所有Three.js中的对象都将被当作原生的JSX元素,就像在ReactDom中写<div />及<span />元素一样。R3F Fiber组件的通用规则是将Three.js中的它们的名字写成驼峰式的DOM元素即可。
一个 Mesh 是 Three.js 中的基础场景对象,需要给它提供一个几何对象 geometry 以及一个材质 material 来代表一个三维空间的几何形状,我们将使用一个 BoxGeometry 和 MeshStandardMaterial 来创建一个新的网格 Mesh,它们会自动关联到它们的父节点。
<Canvas>
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
上述代码和以下 Three.js 代码是等价的:
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(width, height)
document.querySelector('#canvas-container').appendChild(renderer.domElement)
const mesh = new THREE.Mesh()
mesh.geometry = new THREE.BoxGeometry()
mesh.material = new THREE.MeshStandardMaterial()
scene.add(mesh)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
}
animate()
构造函数参数:
根据 BoxGeometry 的文档,我们可以选择给它传递三个参数:width、length 及 depth:
new THREE.BoxGeometry(2, 2, 2)
为了实现相同的功能,我们可以在 R3F 中使用 args 属性,它总是接受一个数组,其项目表示构造函数参数:
<boxGeometry args={[2, 2, 2]} />
接着,我们通过像下面这样添加光源组件来为我们的场景添加一些光线。
<Canvas>
<ambientLight intensity={0.1} />
<directionalLight color="red" position={[0, 0, 5]} />
属性:
这里介绍关于 R3F 的最后一个概念,即 React 属性是如何在 Three.js 对象中工作的。当你给一个 Fiber 组件设置任意属性时,它将对 Three.js 设置一个相同名字的属性。我们关注到 ambientLight 上,由它的文档可知,我们可以选择 color 和 intensity 属性来初始化它:
<ambientLight intensity={0.1} />
等价于
const light = new THREE.AmbientLight()
light.intensity = 0.1
快捷方法:
在 Three.js 中对于很多属性的设置如 colors、vectors 等都可以使用 set() 方法进行快捷设置:
const light = new THREE.DirectionalLight()
light.position.set(0, 0, 5)
light.color.set('red')
在 JSX 中也是相同的:
<directionalLight position={[0, 0, 5]} color="red" />
<Canvas>
<mesh>
<boxBufferGeometry />
<meshBasicMaterial color="#03c03c" />
</mesh>
<ambientLight args={[0xff0000]} intensity={0.1} />
<directionalLight position={[0, 0, 5]} intensity={0.5} />
</Canvas>

查看React Three Fiber完整API文档
到这里,我们已经掌握了 R3F 的基本知识,我们再结合专栏上篇关于物理特性的内容,来实现如文章开头介绍的乒乓球 ? 小游戏。
?本文乒乓球小游戏基础版及乒乓球三维模型资源来源于R3F官网示例。
首先,我们创建一个 Experience 文件作为渲染三维场景的组件,并在其中添加 Canvas 组件搭建基本页面结构。
import { Canvas } from "@react-three/fiber";
export default function Experience() {
return (
<>
<Canvas></Canvas>
</>
);
}

接着我们开启 Canvas 的阴影并设置相机参数,然后添加环境光 ambientLight 和点光源 pointLight 两种光源:
<Canvas
shadows
camera={{ fov: 50, position: [0, 5, 12] }}
>
<ambientLight intensity={.5} />
<pointLight position={[-10, -10, -10]} />
</Canvas>
如果需要修改 Canvas 的背景色,可以在其中添加一个 color 标签并设置参数 attach 为 background,在 args 参数中设置颜色即可。
<Canvas>
<color attach="background" args={["lightgreen"]} />
</Canvas>

接着,我们在页面顶部引入 Perf,它是 R3F 生态中查看页面性能的组件,它的功能和 Three.js 中 stats.js 是类似的,像下面这样添加到代码中设置它的显示位置,页面对应区域就会出现可视化的查看工具,在上面可以查看 GPU、CPU、FPS 等性能参数。
如果想使用网格作为辅助线或用作装饰,可以使用 gridHelper 组件,它支持配置 position、rotation、args 等参数。
import { Perf } from "r3f-perf";
export default function Experience() {
return (
<>
<Canvas>
<Perf position="top-right" />
<gridHelper args={[50, 50, '#11f1ff', '#0b50aa']} position={[0, -1.1, -4]} rotation={[Math.PI / 2.68, 0, 0]} />
</Canvas>
</>
);
}

我们创建一个名为 PingPong.jsx 的乒乓球组件文件,然后在文件顶部引入以下依赖,其中 Physics、useBox、usePlane、useSphere 用于创建物理世界;useFrame 是用来进行页面动画更新的 hook,它将在页面每帧重绘时执行,我们可以在它里面执行一些动画函数和更新控制器,相当于 Three.js 中用原生实现的 requestAnimationFrame;useLoader 用于加载器的管理,使用它更方便进行加载错误管理和回调方法执行;lerp 是一个插值运算函数,它可以计算某一数值到另一数值的百分比,从而得出一个新的数值,常用于移动物体、修改透明度、颜色、大小、模拟动画等。
import { Physics, useBox, usePlane, useSphere } from "@react-three/cannon";
import { useFrame, useLoader } from "@react-three/fiber";
import { Mesh, TextureLoader } from "three";
import { GLTFLoader } from "three-stdlib/loaders/GLTFLoader";
import lerp from "lerp";
然后创建一个 PingPong 类,在其中添加 <Physics> 组件来创建物理世界,像直接使用 Cannon.js 一样,可以给它设置 iterations、tolerance、gravity、allowSleep 等参数来分别设置物理世界的迭代次数、容错性、引力以及是否支持进入休眠状态等,然后在其中添加一个平面几何体和一个平面刚体 ContactGround。
function ContactGround() {
const [ref] = usePlane(
() => ({
position: [0, -10, 0],
rotation: [-Math.PI / 2, 0, 0],
type: "Static",
}),
useRef < Mesh > null
);
return <mesh ref={ref} />;
}
export default function PingPong() {
return (
<>
<Physics
iterations={20}
tolerance={0.0001}
defaultContactMaterial={{
contactEquationRelaxation: 1,
contactEquationStiffness: 1e7,
friction: 0.9,
frictionEquationRelaxation: 2,
frictionEquationStiffness: 1e7,
restitution: 0.7,
}}
gravity={[0, -40, 0]}
allowSleep={false}
>
<mesh position={[0, 0, -10]} receiveShadow>
<planeGeometry args={[1000, 1000]} />
<meshPhongMaterial color="#5081ca" />
</mesh>
<ContactGround />
</Physics>
</>
);
}

接着,我们创建一个球体类 Ball,在其中添加球体 ? ,可以使用前面介绍的 useLoader 来管理它的贴图加载,为了方便观察到乒乓球的转动情况,贴图中央加了一个十字交叉图案 ➕。然后将其放在 <Physics> 标签下。
function Ball() {
const map = useLoader(TextureLoader, earthImg);
const [ref] = useSphere(
() => ({ args: [0.5], mass: 1, position: [0, 5, 0] }),
useRef < Mesh > null
);
return (
<mesh castShadow ref={ref}>
<sphereGeometry args={[0.5, 64, 64]} />
<meshStandardMaterial map={map} />
</mesh>
);
}
export default function PingPong() {
return (
<>
<Physics>
{ /* ... */ }
<Ball />
</Physics>
</>
);
}

球拍 ? 采用的是一个 glb 格式的模型,在 Blender 中我们可以看到模型的样式和详细的骨骼结构,对于模型的加载,我们同样使用 useLoader 来管理,此时的加载器需要使用 GLTFLoader。

我们创建一个 Paddle 类并将其添加到 <Physics> 标签中,在这个类中我们实现模型加载,模型加载完成后绑定骨骼,并在 useFrame 页面重绘方法中,根据鼠标所在位置更新乒乓球拍模型的位置 position,并根据是否一开始游戏状态以及鼠标的位置来更新球拍的 x轴 和 y轴 方向的 rotation 值。
function Paddle() {
const { nodes, materials } = useLoader(
GLTFLoader,
'/models/pingpong.glb',
);
const model = useRef();
const [ref, api] = useBox(() => ({
type: 'Kinematic',
args: [3.4, 1, 3.5],
}));
const values = useRef([0, 0]);
useFrame((state) => {
values.current[0] = lerp(
values.current[0],
(state.mouse.x * Math.PI) / 5,
0.2
);
values.current[1] = lerp(
values.current[1],
(state.mouse.x * Math.PI) / 5,
0.2
);
api.position.set(state.mouse.x * 10, state.mouse.y * 5, 0);
api.rotation.set(0, 0, values.current[1]);
if (!model.current) return;
model.current.rotation.x = lerp(
model.current.rotation.x,
started ? Math.PI / 2 : 0,
0.2
);
model.current.rotation.y = values.current[0];
});
return (
<mesh ref={ref} dispose={null}>
<group
ref={model}
position={[-0.05, 0.37, 0.3]}
scale={[0.15, 0.15, 0.15]}
>
<group rotation={[1.88, -0.35, 2.32]} scale={[2.97, 2.97, 2.97]}>
<primitive object={nodes.Bone} />
<primitive object={nodes.Bone003} />
{ /* ... */ }
<skinnedMesh
castShadow
receiveShadow
material={materials.glove}
material-roughness={1}
geometry={nodes.arm.geometry}
skeleton={nodes.arm.skeleton}
/>
</group>
<group rotation={[0, -0.04, 0]} scale={[141.94, 141.94, 141.94]}>
<mesh
castShadow
receiveShadow
material={materials.wood}
geometry={nodes.mesh.geometry}
/>
{ /* ... */ }
</group>
</group>
</mesh>
);
}
到这里,我们已经实现乒乓球颠球的基本功能了 ?

为了显示每次游戏可以颠球的次数,现在我们在乒乓球拍中央加上数字显示 5️⃣ 。我们可以像下面这样创建一个 Text 类,在文件顶部引入 TextGeometry、FontLoader、fontJson 作为字体几何体、字体加载器以及字体文件,添加一个 geom 作为创建字体几何体的方法,当 count 状态值发生变化时,实时更新创建字体几何体模型。
import { useMemo } from "react";
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader";
import fontJson from "../public/fonts/firasans_regular.json";
const font = new FontLoader().parse(fontJson);
const geom = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'].map(
(number) => new TextGeometry(number, { font, height: 0.1, size: 5 })
);
export default function Text({ color = 0xffffff, count, ...props }) {
const array = useMemo(() => [...count], [count]);
return (
<group {...props} dispose={null}>
{array.map((char, index) => (
<mesh
position={[-(array.length / 2) * 3.5 + index * 3.5, 0, 0]}
key={index}
geometry={geom[parseInt(char)]}
>
<meshBasicMaterial color={color} transparent opacity={0.5} />
</mesh>
))}
</group>
);
}
然后将 Text 字体类放入球拍几何体中,其中 count 字段需要在物理世界中刚体发生碰撞时进行更新,该方法加载下节内容添加碰撞音效时一起实现。
function Paddle() {
return (
<mesh ref={ref} dispose={null}>
<group ref={model}>
{ /* ... */ }
<Text
rotation={[-Math.PI / 2, 0, 0]}
position={[0, 1, 2]}
count={count.toString()}
/>
</group>
</mesh>
);
}

到这里,整个小游戏的全部流程都开发完毕了,现在我们来加一些页面提示语、颠球时的碰撞音效,页面的光照效果等,使 3D 场景看起来更加真实。
实现音效 ? 前,我们先像下面这样添加一个状态管理器 ? ,来进行页面全局状态的管理。zustand 是一个轻量级的状态管理库;_.clamp(number, [lower], upper) 用于返回限制在 lower 和 upper 之间的值;pingSound 是需要播放的音频文件。我们在其中添加一个 pong 方法用来更新音效和颠球计数,添加一个 reset 方法重置颠球数字。count 字段表示每次的颠球次数,welcome 表示是否在欢迎界面。
import create from "zustand";
import clamp from "lodash-es/clamp";
import pingSound from "/medias/ping.mp3";
const ping = new Audio(pingSound);
export const useStore = create((set) => ({
api: {
pong(velocity) {
ping.currentTime = 0;
ping.volume = clamp(velocity / 20, 0, 1);
ping.play();
if (velocity > 4) set((state) => ({ count: state.count + 1 }));
},
reset: (welcome) =>
set((state) => ({ count: welcome ? state.count : 0, welcome })),
},
count: 0,
welcome: true,
}));
然后我们可以在上述 Paddle 乒乓球拍类中像这样在物体发生碰撞时触发 pong 方法:
function Paddle() {
{/* ... */}
const [ref, api] = useBox(() => ({
type: "Kinematic",
args: [3.4, 1, 3.5],
onCollide: (e) => pong(e.contact.impactVelocity),
}));
}
为了是场景更加真实,我们可以开启 Canvas 的阴影,然后添加多种光源 ? 来优化场景,如 spotLight 就能起到视觉聚焦的作用。
<Canvas
shadows
camera={{ fov: 50, position: [0, 5, 12] }}
>
<ambientLight intensity={.5} />
<pointLight position={[-10, -10, -10]} />
<spotLight
position={[10, 10, 10]}
angle={0.3}
penumbra={1}
intensity={1}
castShadow
shadow-mapSize-width={2048}
shadow-mapSize-height={2048}
shadow-bias={-0.0001}
/>
<PingPong />
</Canvas>
为了提升小游戏的用户体验,我们可以添加一些页面文字提示来指引使用者和提升页面视觉效果,需要注意的是,这些额外的元素不能添加到 <Canvas /> 标签内哦 ?。
const style = (welcome) => ({
color: '#000000',
display: welcome ? 'block' : 'none',
fontSize: '1.8em',
left: '50%',
position: "absolute",
top: 40,
transform: 'translateX(-50%)',
background: 'rgba(255, 255, 255, .2)',
backdropFilter: 'blur(4px)',
padding: '16px',
borderRadius: '12px',
boxShadow: '1px 1px 2px rgba(0, 0, 0, .2)',
border: '1px groove rgba(255, 255, 255, .2)',
textShadow: '0px 1px 2px rgba(255, 255, 255, .2), 0px 2px 2px rgba(255, 255, 255, .8), 0px 2px 4px rgba(0, 0, 0, .5)'
});
<div style={style(welcome)}>? 点击任意区域开始颠球</div>

本文中主要包含的知识点包括:
React Three Fiber 及相关生态。React Three Fiber 基础入门。React Three Fiber 开发一个乒乓球小游戏,学会如何场景构建、模型加载、物理世界关联、全局状态管理等。想了解其他前端知识或其他未在本文中详细描述的Web 3D开发技术相关知识,可阅读我往期的文章。如果有疑问可以在评论中留言,如果觉得文章对你有帮助,不要忘了一键三连哦 ?。
...本文作者:dragonir 本文地址:https://www.cnblogs.com/dragonir/p/17235128.html
让我们计算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
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u
之前说过10之后的版本没有3dScan了,所以还是9.8的版本或者之前更早的版本。 3d物体扫描需要先下载扫描的APK进行扫面。首先要在手机上装一个扫描程序,扫描现实中的三维物体,然后上传高通官网,在下载成UnityPackage类型让Unity能够使用这个扫描程序可以从高通官网上进行下载,是一个安卓程序。点到Tools往下滑,找到VuforiaObjectScanner下载后解压数据线连接手机,将apk文件拷入手机安装然后刚才解压文件中的Media文件夹打开,两个PDF图打印第一张A4-ObjectScanningTarget.pdf,主要是用来辅助扫描的。好了,接下来就是扫描三维物体。将瓶
修改(澄清问题)我已经花了几天时间试图弄清楚如何从Facebook游戏中抓取特定信息;但是,我遇到了一堵又一堵砖墙。据我所知,主要问题如下。我可以使用Chrome的检查元素工具手动查找我需要的html-它似乎位于iframe中。但是,当我尝试抓取该iframe时,它是空的(属性除外):如果我使用浏览器的“查看页面源代码”工具,这与我看到的输出相同。我不明白为什么我看不到iframe中的数据。答案不是它是由AJAX之后添加的。(我知道这既是因为“查看页面源代码”可以读取Ajax添加的数据,也是因为我有b/c我一直等到我可以看到数据页面之后才抓取它,但它仍然不存在)。发生这种情况是因为
我开始了一个新的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
rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:
关闭。这个问题不符合StackOverflowguidelines.它目前不接受答案。要求我们推荐或查找工具、库或最喜欢的场外资源的问题对于StackOverflow来说是偏离主题的,因为它们往往会吸引自以为是的答案和垃圾邮件。相反,describetheproblem以及迄今为止为解决该问题所做的工作。关闭9年前。Improvethisquestion是否有适用于这些的3d游戏引擎?
我有一个包含多个组件的存储库,其中大部分是用JavaScript(Node.js)编写的,一个是用Ruby(RubyonRails)编写的。我想要一个.travis.yml文件来触发一个运行每个组件的所有测试的构建。根据thisTravisCIGoogleGroupthread,目前还没有官方支持。我的目录结构是这样的:.├──构建服务器├──核心├──扩展├──网络应用├──流浪文件├──package.json├──.travis.yml└──生成文件我希望能够运行特定版本的Ruby(2.2.2)和Node.js(0.12.2)。我已经有了一个make目标,所以maketest在每