大家好,我是考100分的小小码 ,祝大家学习进步,加薪顺利呀。今天说一说n阶魔方块数_n阶魔方阵,希望您对编程的造诣更进一步.
相信很多同学都玩过魔方,那么你知道如何开发一个魔方的游戏么?今天这篇文章为大家带来使用 Oasis 快速实现 N 阶魔方的总结分享,感谢蚂蚁集团工程师箕宿的投稿和分享,也欢迎广大读者的交流反馈~
Oasis 是一个移动优先的高性能引擎,支付宝里面的很多互动业务都是拿 Oasis 制作的。因此选择 Oasis 作为开发 N 阶魔方的引擎(本文使用引擎版本为 0.5.6)。
实体与组件
Oasis 有两个重要的概念
- Entity 每个场景下的都会有实体的树形,但是其本身不具备渲染等实际的功能
- Component 是具有实际功能的,可以被 Entity 添加
这种基于组件的功能扩展方式注重将程序按照功能独立封装,在使用的时候按照需要组合添加,非常有利于降低程序耦合度并提升代码复用率。
搭建项目
Oasis 自带一个脚手架可以直接用来搭建一个项目, npm init @oasisi-engine/oasis-app
添加摄像机控制
摄像头的控制器在 @oasis-engine/controls 这个包里
// init camera
const cameraEntity = rootEntity.createChild("camera");
const camera = cameraEntity.addComponent(Camera);
// add camera control
cameraEntity.addComponent(OrbitControl);
const pos = cameraEntity.transform.position;
pos.setValue(10, 10, 10);
cameraEntity.transform.position = pos;
cameraEntity.transform.lookAt(new Vector3(0, 0, 0));
使用 bufferMesh 绘制魔方用的方块
bufferMesh 可以自由的控制顶点,法线和颜色, 绘制立方体的顶点。这里用 8 个顶角和 12 个棱以及 6 个面绘制魔方中的一个单独的方块, 通过顶点和棱绘制出边框的效果
import {
Buffer,
BufferBindFlag,
BufferMesh,
BufferUsage,
IndexFormat,
VertexElement,
Engine,
Color,
VertexElementFormat,
MeshTopology,
} from "oasis-engine";
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return new Color(
parseInt(result[1], 16) / 255,
parseInt(result[2], 16) / 255,
parseInt(result[3], 16) / 255,
1.0,
);
}
const colorList = [
hexToRgb('#C41E3A'),
hexToRgb('#009E60'),
hexToRgb('#0051BA'),
hexToRgb('#FF5800'),
hexToRgb('#FFD500'),
hexToRgb('#FFFFFF')
];
export function createCube(engine: Engine, size: number, strokeSize: number) {
const geometry = new BufferMesh(engine, "CustomCubeGeometry");
const smallSize = size - strokeSize;
// prettier-ignore
// Create vertices data.
const vertices: Float32Array = new Float32Array([
// Up
-smallSize, size, -smallSize, 0, 1, 0, smallSize, size, -smallSize, 0, 1, 0, smallSize, size, smallSize, 0, 1, 0, -smallSize, size, smallSize, 0, 1, 0,
// Down
-smallSize, -size, -smallSize, 0, -1, 0, smallSize, -size, -smallSize, 0, -1, 0, smallSize, -size, smallSize, 0, -1, 0, -smallSize, -size, smallSize, 0, -1, 0,
// Left
-size, smallSize, -smallSize, -1, 0, 0, -size, smallSize, smallSize, -1, 0, 0, -size, -smallSize, smallSize, -1, 0, 0, -size, -smallSize, -smallSize, -1, 0, 0,
// Right
size, smallSize, -smallSize, 1, 0, 0, size, smallSize, smallSize, 1, 0, 0, size, -smallSize, smallSize, 1, 0, 0, size, -smallSize, -smallSize, 1, 0, 0,
// Front
-smallSize, smallSize, size, 0, 0, 1, smallSize, smallSize, size, 0, 0, 1, smallSize, -smallSize, size, 0, 0, 1, -smallSize, -smallSize, size, 0, 0, 1,
// Back
-smallSize, smallSize, -size, 0, 0, -1, smallSize, smallSize, -size, 0, 0, -1, smallSize, -smallSize, -size, 0, 0, -1, -smallSize, -smallSize, -size, 0, 0, -1,
// 八个角
// up left back
-smallSize, size, -smallSize, -1, 1, -1, -size, smallSize, -smallSize, -1, 1, -1, -smallSize, smallSize, -size, -1, 1, -1,
// down left back
-smallSize, -size, -smallSize, -1, -1, -1, -size, -smallSize, -smallSize, -1, -1, -1, -smallSize, -smallSize, -size, -1, -1, -1,
// up right back
smallSize, size, -smallSize, 1, 1, -1, size, smallSize, -smallSize, 1, 1, -1, smallSize, smallSize, -size, 1, 1, -1,
// down right back
smallSize, -size, -smallSize, 1, -1, -1, size, -smallSize, -smallSize, 1, -1, -1, smallSize, -smallSize, -size, 1, -1, -1,
// up left front
-smallSize, size, smallSize, -1, 1, 1, -size, smallSize, smallSize, -1, 1, 1, -smallSize, smallSize, size, -1, 1, 1,
// down left front
-smallSize, -size, smallSize, -1, -1, 1, -size, -smallSize, smallSize, -1, -1, 1, -smallSize, -smallSize, size, -1, -1, 1,
// up right front
smallSize, size, smallSize, 1, 1, 1, size, smallSize, smallSize, 1, 1, 1, smallSize, smallSize, size, 1, 1, 1,
// down right front
smallSize, -size, smallSize, 1, -1, 1, size, -smallSize, smallSize, 1, -1, 1, smallSize, -smallSize, size, 1, -1, 1,
// 12个棱
// up left
-smallSize, size, -smallSize, -1, 1, 0, -smallSize, size, smallSize, -1, 1, 0, -size, smallSize, -smallSize, -1, 1, 0, -size, smallSize, smallSize, -1, 1, 0,
// up right
smallSize, size, -smallSize, 1, 1, 0, smallSize, size, smallSize, 1, 1, 0, size, smallSize, -smallSize, 1, 1, 0, size, smallSize, smallSize, 1, 1, 0,
// up front
-smallSize, size, smallSize, 0, 1, 1, smallSize, size, smallSize, 0, 1, 1, -smallSize, smallSize, size, 0, 1, 1, smallSize, smallSize, size, 0, 1, 1,
// up back
-smallSize, size, -smallSize, 0, 1, -1, smallSize, size, -smallSize, 0, 1, -1, -smallSize, smallSize, -size, 0, 1, -1, smallSize, smallSize, -size, 0, 1, -1,
// down left
-smallSize, -size, -smallSize, -1, -1, 0, -smallSize, -size, smallSize, -1, -1, 0, -size, -smallSize, -smallSize, -1, -1, 0, -size, -smallSize, smallSize, -1, -1, 0,
// down right
smallSize, -size, -smallSize, 1, -1, 0, smallSize, -size, smallSize, 1, -1, 0, size, -smallSize, -smallSize, 1, -1, 0, size, -smallSize, smallSize, 1, -1, 0,
// down front
-smallSize, -size, smallSize, 0, -1, 1, smallSize, -size, smallSize, 0, -1, 1, -smallSize, -smallSize, size, 0, -1, 1, smallSize, -smallSize, size, 0, -1, 1,
// down back
-smallSize, -size, -smallSize, 0, -1, -1, smallSize, -size, -smallSize, 0, -1, -1, -smallSize, -smallSize, -size, 0, -1, -1, smallSize, -smallSize, -size, 0, -1, -1,
// left front
-size, smallSize, smallSize, -1, 0, 1, -size, -smallSize, smallSize, -1, 0, 1, -smallSize, smallSize, size, -1, 0, 1, -smallSize, -smallSize, size, -1, 0, 1,
// front right
smallSize, smallSize, size, 1, 0, 1, smallSize, -smallSize, size, 1, 0, 1, size, smallSize, smallSize, 1, 0, 1, size, -smallSize, smallSize, 1, 0, 1,
// right back
size, smallSize, -smallSize, 1, 0, -1, size, -smallSize, -smallSize, 1, 0, -1, smallSize, smallSize, -size, 1, 0, -1, smallSize, -smallSize, -size, 1, 0, -1,
// back left
-smallSize, smallSize, -size, -1, 0, -1, -smallSize, -smallSize, -size, -1, 0, -1, -size, smallSize, -smallSize, -1, 0, -1, -size, -smallSize, -smallSize, -1, 0, -1,
]);
// prettier-ignore
// Create indices data.
const indices: Uint16Array = new Uint16Array([
// Up
0, 2, 1, 2, 0, 3,
// Down
4, 6, 7, 6, 4, 5,
// Left
8, 10, 9, 10, 8, 11,
// Right
12, 14, 15, 14, 12, 13,
// Front
16, 18, 17, 18, 16, 19,
// Back
20, 22, 23, 22, 20, 21,
// up left Back corner
24, 26, 25,
// down left back corner
27, 28, 29,
// up right back
30, 31, 32,
// down right back
33, 35, 34,
// up left front
36, 37, 38,
// down left front
39, 41, 40,
// up right front
42, 44, 43,
// down right front
45, 46, 47,
// Up left
48, 50, 49, 50, 51, 49,
// up right
52, 53, 54, 55, 54, 53,
// up front
56, 58, 57, 58, 59, 57,
// up back
60, 61, 62, 62, 61, 63,
// down left
64, 65, 66, 66, 65, 67,
// down right
68, 70, 69, 71, 69, 70,
// down front
72, 73, 74, 75, 74, 73,
// down back
76, 78, 77, 79, 77, 78,
// left front
80, 81, 82, 83, 82, 81,
// front right
84, 85, 86, 87, 86, 85,
// right back
88, 89, 90, 91, 90, 89,
// back left
92, 93, 94, 95, 94, 93,
]);
// Create vertices color and init by black.
const colorData = new Float32Array(3 * 24 + 3 * 24 + 3 * 32 + 3 * 16);
colorData.fill(0.0);
for (let i = 0; i < 6; i++) {
const color = colorList[i];
const offset = i * 12;
for (let i = 0; i < 4; i++) {
colorData[offset + i * 3 + 0] = color.r;
colorData[offset + i * 3 + 1] = color.g;
colorData[offset + i * 3 + 2] = color.b;
}
}
// Create gpu vertex buffer and index buffer.
const vertexBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, vertices, BufferUsage.Static);
const indexBuffer = new Buffer(engine, BufferBindFlag.IndexBuffer, indices, BufferUsage.Static);
const independentColorBuffer = new Buffer(engine, BufferBindFlag.VertexBuffer, colorData, BufferUsage.Dynamic);
// Bind buffer
geometry.setVertexBufferBinding(vertexBuffer, 24);
geometry.setIndexBufferBinding(indexBuffer, IndexFormat.UInt16);
geometry.setVertexBufferBinding(independentColorBuffer, 12, 1);
// Add vertexElement
geometry.setVertexElements([
new VertexElement("POSITION", 0, VertexElementFormat.Vector3, 0),
new VertexElement("NORMAL", 12, VertexElementFormat.Vector3, 0),
new VertexElement("COLOR_0", 0, VertexElementFormat.Vector3, 1)
]);
// Add one sub geometry.
geometry.addSubMesh(0, indices.length, MeshTopology.Triangles);
return geometry;
}
绘制 3 阶魔方
从 xyz 轴 -1 0 1,三次 for 循环绘制 27 个小方块
for (let i = -1; i < 2; i++) {
for (let j = -1; j < 2; j++) {
for (let k = -1; k < 2; k++) {
const cubeEntity = rootEntity.createChild("cube");
const cubePos = cubeEntity.transform.position;
cubePos.setValue(i * cubeSize, j * cubeSize, k * cubeSize);
cubeEntity.transform.position = cubePos;
// use to rayrast pick
const boxCollider = cubeEntity.addComponent(BoxCollider);
boxCollider.setBoxCenterSize(new Vector3(), new Vector3(cubeSize, cubeSize, cubeSize));
const renderer = cubeEntity.addComponent(MeshRenderer);
const mtl = new BlinnPhongMaterial(engine);
renderer.mesh = createCube(engine, cubeSize/2, CUBE_STROKE);
renderer.setMaterial(mtl);
}
}
}
拾取鼠标点击的方块
通过摄像机的 raycast 和 collider 可以快速拾取 entity,同时可以获取到焦点。由于魔方只有 6 个面,那么肯定存在交点的 (x,y,z) 其中一个等于边界值的情况,根据这个边界值就可以判断点击的是哪个面。
const ray = new Ray();
const ratio = window.devicePixelRatio;
// 鼠标点击触发拾取
function handleMouseDown(e) {
let x = e.offsetX;
let y = e.offsetY;
if (e.touches && e.touches.length > 0) {
x = e.touches[0].pageX;
y = e.touches[0].pageY;
}
camera.screenPointToRay(new Vector2(x, y).scale(ratio), ray);
const hit = new HitResult();
const result = engine.physicsManager.raycast(ray, Number.MAX_VALUE,
Layer.Everything, hit);
if (result) {
console.info('点击的entity',hit.collider.entity)
console.info('点击的相交点',hit.point)
}
}
document.getElementById("canvas").addEventListener("mousedown", handleMouseDown);
document.getElementById("canvas").addEventListener("touchstart", handleMouseDown);
判断是哪个面
// 拾取面 上下左右前后
export enum DIR {
UP = 'UP',
DOWN = 'DOWN',
LEFT = 'LEFT',
RIGHT = 'RIGHT',
FRONT = 'FRONT',
BACK = 'BACK',
}
// 根据点判断是哪个面
function getDir(point:Vector3) {
const maxDist = CUBE_LEVEL * CUBE_SIZE / 2;
const delta = 0.001;
const dx = Math.abs(Math.abs(point.x) - maxDist);
const dy = Math.abs(Math.abs(point.y) - maxDist);
const dz = Math.abs(Math.abs(point.z) - maxDist);
if (dx < delta) {
if (point.x > 0) {
return DIR.RIGHT;
} else {
return DIR.LEFT;
}
} else if (dy < delta) {
if (point.y > 0) {
return DIR.UP;
} else {
return DIR.DOWN;
}
} else if (dz < delta) {
if (point.z > 0) {
return DIR.BACK;
}
}
return DIR.FRONT;
}
判定旋转方向
拾取的魔方中的一个方块的其中一个面,那么旋转只存在两种情况,一种是横着旋转,一种是竖着旋转。如果是垂直于 x 轴的面的话,那么只存在绕着 y 轴或者绕着 z 轴两种情况。
通过 rayrast 可以获取到一个从屏幕点击点发出的射线,在鼠标按下和鼠标抬起的分别取射线的 direction
判断两次 direction 移动的在 xyz 的方向最大的一个就可以拿到用户要进行的旋转方向。
整个魔方只有6个旋转方向
class ROTATION {
// x轴顺时针
static X = new Vector3(1, 0, 0);
// x轴逆时针
static DX = new Vector3(-1, 0, 0);
// y轴顺时针
static Y = new Vector3(0, 1, 0);
// y轴逆时针
static DY = new Vector3(0, -1, 0);
// z轴顺时针
static Z = new Vector3(0, 0, 1);
// z轴逆时针
static DZ = new Vector3(0, 0, -1);
}
根据面和 direction 获取要进行的旋转轴
function getRotation(startDirection: Vector3, endDirection: Vector3): Vector3 {
const { x, y, z } = startDirection.clone().subtract(endDirection);
switch (this.dir) {
case DIR.UP:
if (Math.abs(x) > Math.abs(z)) {
if (x > 0) {
return ROTATION.DZ;
} else {
return ROTATION.Z;
}
} else {
if (z > 0) {
return ROTATION.X;
} else {
return ROTATION.DX;
}
}
case DIR.DOWN:
if (Math.abs(x) > Math.abs(z)) {
if (x > 0) {
return ROTATION.Z;
} else {
return ROTATION.DZ;
}
} else {
if (z > 0) {
return ROTATION.DX;
} else {
return ROTATION.X;
}
}
case DIR.LEFT:
if (Math.abs(y) > Math.abs(z)) {
if (y > 0) {
return ROTATION.DZ;
} else {
return ROTATION.Z;
}
} else {
if (z > 0) {
return ROTATION.Y;
} else {
return ROTATION.DY;
}
}
case DIR.RIGHT:
if (Math.abs(y) > Math.abs(z)) {
if (y > 0) {
return ROTATION.Z;
} else {
return ROTATION.DZ;
}
} else {
if (z > 0) {
return ROTATION.DY;
} else {
return ROTATION.Y;
}
}
case DIR.FRONT:
if (Math.abs(x) > Math.abs(y)) {
if (x > 0) {
return ROTATION.DY;
} else {
return ROTATION.Y;
}
} else {
if (y > 0) {
return ROTATION.X;
} else {
return ROTATION.DX;
}
}
case DIR.BACK:
if (Math.abs(x) > Math.abs(y)) {
if (x > 0) {
return ROTATION.Y;
} else {
return ROTATION.DY;
}
} else {
if (y > 0) {
return ROTATION.DX;
} else {
return ROTATION.X;
}
}
}
}
旋转
点击的方块和旋转轴,就可以旋转了,每次旋转都是旋转在同一个面的方块。
获取要旋转的方块。
function near(a: number, b: number): boolean {
return Math.abs(a - b) < 0.0001;
}
const cubes = cubes.filter(v => {
if (this.rotation === ROTATION.X || this.rotation === ROTATION.DX) {
return near(v.transform.position.x, entity.transform.position.x);
} else if (this.rotation === ROTATION.Y || this.rotation === ROTATION.DY) {
return near(v.transform.position.y, entity.transform.position.y);
}
return near(v.transform.position.z, entity.transform.position.z);
});
// 记录下旋转开始前的方块的position和rotation
const rotationQuaternions = cubes.map(v => v.transform.rotationQuaternion.clone());
const positions = cubes.map(v => v.transform.position.clone());
旋转的动画
const angle = 0;
let start = true;
function update() {
if(!start) return;
angle += ROTATION_SPEED;
if (angle > 90) {
angle = 90;
start = false;
}
cubes.forEach((cube, index) => {
const matrix = new Matrix();
const position = positions[index].clone();
const quaternion = rotationQuaternions[index].clone();
const { x, y, z } = position;
matrix.translate(new Vector3(-x, -y, -z))
matrix.rotateAxisAngle(this.rotation, this.radians);
matrix.translate(new Vector3(x, y, z));
// 更新position
cube.transform.position = position.transformNormal(matrix);
const q = new Quaternion;
q.rotateAxisAngle(this.rotation, this.radians);
// 更新旋转
cube.transform.rotationQuaternion = q.multiply(quaternion);
});
}
旋转的动画还有一种优化方案,把所有要旋转的方块当一个整体,绕某个轴旋转,这样只需要更新下每个方块的世界矩阵即可,具体代码如下
const angle = 0;
let start = true;
const tempMat1 = new Matrix();
const tempMat2 = new Matrix();
function update() {
if(!start) return;
angle += ROTATION_SPEED;
let delta = ROTATION_SPEED;
if (angle > 90) {
delta = 90 - (angle - ROTATION_SPEED);
angle = 90;
start = false;
}
tempMat1.identity().rotateAxisAngle(this.rotation, delta * Math.PI / 180 );
cubes.forEach((cube, index) => {
// 更新世界矩阵
Matrix.multiply(tempMat1, cube.transform.worldMatrix, tempMat2);
cube.transform.worldMatrix = tempMat2;
});
}
扩展到 N 阶魔方
上述算法与魔方的阶数没有太大关联,只要在创建的时候指定好魔方块的位置就可以实现 N 阶段魔方。
奇数魔方和偶数魔方要注意整体相对于(0,0,0)对称摆放
const CUBE_SIZE = 1.0;
const CUBE_LEVEL = 10;
function createRuickCube(rootEntity: Entity, engine: Engine) {
const isEven = CUBE_LEVEL % 2 === 0;
let start = - Math.floor(CUBE_LEVEL / 2);
let end = Math.ceil(CUBE_LEVEL / 2);
let offset = 0;
if (isEven) {
start += 1;
end += 1;
offset = -0.5;
}
for (let i = start; i < end; i++) {
for (let j = start; j < end; j++) {
for (let k = start; k < end; k++) {
const cubeEntity = rootEntity.createChild("cube");
const cubePos = cubeEntity.transform.position;
cubePos.setValue((i + offset) * CUBE_SIZE, (j + offset) * CUBE_SIZE, (k + offset) * CUBE_SIZE);
cubeEntity.transform.position = cubePos;
const boxCollider = cubeEntity.addComponent(BoxCollider);
boxCollider.setBoxCenterSize(new Vector3(), new Vector3(CUBE_SIZE, CUBE_SIZE, CUBE_SIZE));
const renderer = cubeEntity.addComponent(MeshRenderer);
const mtl = new BlinnPhongMaterial(engine);
renderer.mesh = createCube(engine, CUBE_SIZE / 2, CUBE_STROKE);
renderer.setMaterial(mtl);
}
}
}
}
源码
演示地址
如何进一步了解我们
Oasis 开源社区群 (钉钉):
Oasis 开源社区群管理员 (微信):
网站
官网地址
oasisengine.cn
Engine 源码地址
github.com/oasis-engin…
Engine Toolkit 源码地址
github.com/oasis-engin…
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13090.html