已更新。由于另一个类似问题的答案基于此先前版本的答案,因此我已将答案更改为更好的答案。
变换、加速、阻力和火箭飞船。
有很多方法可以将运动应用到小行星类型的游戏中。这个答案展示了最基本的方法,然后给出了一个例子,展示了产生不同感觉的基本方法的变化。这个答案还简要概述了如何使用矩阵(2D)设置 CSS 转换
基础知识
在最基本的情况下,您有一个代表位置或旋转的某些组成部分的数字。要移动,请添加一个常量 x += 1; move in x 一次一个单位,当您松开未添加的控件并停止时。
但事情不会那样移动,它们会加速。因此,您创建了第二个值来保存速度(以前来自x += 1)并将其命名为dx(delta X)。当你得到输入时,你会稍微提高速度dx += 0.01,这样速度就会逐渐增加。
但问题是你控制的时间越长,你走得越快,当你松开控制时,飞船会继续前进(这对于太空来说是正常的,但在游戏中很痛苦)所以你需要限制速度并逐渐降低到零。您可以通过对每帧的 delta X 值应用比例来做到这一点。 dx *= 0.99;
这样你就有了基本的加速度、阻力和速度限制值
x += dx;
dx *= 0.99;
if(input){ dx += 0.01);
对 x、y 和角度都这样做。如果输入是有方向的,您需要对 x、y 使用向量,如下所示。
x += dx;
y += dy;
angle += dAngle;
dx *= 0.99;
dy *= 0.99;
dAngle *= 0.99;
if(turnLeft){
dAngle += 0.01;
}
if(turnRight){
dAngle -= 0.01;
}
if(inputMove){
dx += Math.cos(angle) * 0.01;
dy += Math.sin(angle) * 0.01;
}
这是最基本的太空游戏动作。
设置 CSS 变换。
设置 CSS 变换最容易通过 matrix 命令应用。例如设置默认变换element.style.transform = "matrix(1,0,0,1,0,0)";
这六个值通常被命名为 a,b,c,d,e 'matrix(a,b,c,d,e,f)' 或 m11, m12, m21, m22、m31、m32 或
水平缩放、水平倾斜、垂直倾斜、垂直缩放、水平移动、垂直移动并表示 3 x 3 2D 矩阵的缩短版本。
我发现对于这个矩阵如何工作以及为什么它不经常使用的大部分困惑部分是由于变量的命名。我更喜欢 x 轴 x,x 轴 y,y 轴 x,y 轴 y,原点 x,原点 y 的描述,并简单地描述 x 和 y 轴的方向和比例以及原点的位置都在 CSS 像素坐标中.
下图说明了矩阵。红框是一个已旋转 45 度(Math.PI / 4 弧度)的元素,其原点移动到 CSS 像素坐标 16,16。
图像 网格显示 CSS 像素。右侧网格显示了矩阵的放大视图,显示了 X 轴矢量 (a,b) = (cos(45), sin(45)), Y 轴矢量 (c,d) = (cos(45 + 90), sin(45 + 90)) 和原点 (e,f) = (16, 16)
因此,我有一个元素的角度、位置 (x,y)、比例 (x,y) 的值。然后我们创建矩阵如下
var scale = { x : 1, y : 1 };
var pos = {x : 16, y : 16 };
var angle = Math.PI / 4; // 45 deg
var a,b,c,d,e,f; // the matrix arguments
// the x axis and x scale
a = Math.cos(angle) * scale.x;
b = Math.sin(angle) * scale.x;
// the y axis which is at 90 degree clockwise of the x axis
// and the y scale
c = -Math.sin(angle) * scale.y;
d = Math.cos(angle) * scale.y;
// and the origin
e = pos.x;
f = pos.y;
element.style.transform = "matrix("+[a,b,c,d,e,f].join(",")+")";
由于大多数时候我们不倾斜变换并使用统一的比例,我们可以缩短代码。我更喜欢使用预定义的数组来帮助保持低 GC 命中率。
const preDefinedMatrix = [1,0,0,1,0,0]; // define at start
// element is the element to set the CSS transform on.
// x,y the position of the elements local origin
// scale the scale of the element
// angle the angle in radians
function setElementTransform (element, x, y, scale, angle) {
var m = preDefinedMatrix;
m[3] = m[0] = Math.cos(angle) * scale;
m[2] = -(m[1] = Math.sin(angle) * scale);
m[4] = x;
m[5] = y;
element.style.transform = "matrix("+m.join(",")+")";
}
我在演示中使用了一个稍微不同的函数。 ship.updatePos 并使用 ship.pos 和 ship.displayAngle 设置相对于包含元素原点(上、左)的变换
注意虽然 3D 矩阵稍微复杂一点(包括投影),但与 2D 矩阵非常相似,它将 x、y 和 z 轴分别描述为 3 个向量具有 3 个标量 (x,y,z),y 轴与 x 轴成 90 度,z 轴与 x 和 y 轴成 90 度,可以通过 x 点 y 轴的叉积找到。每个轴的长度为刻度,原点为点坐标(x,y,z)。
演示:
演示展示了 4 5 个变体。使用键盘 1,2,3,4,5 选择一艘船(它会变成红色)并使用箭头键飞行。基本向上箭头你走,左转右转。
每艘船的数学都在对象ship.controls
requestAnimationFrame(mainLoop);
const keys = {
ArrowUp : false,
ArrowLeft : false,
ArrowRight : false,
Digit1 : false,
Digit2 : false,
Digit3 : false,
Digit4 : false,
Digit5 : false,
event(e){
if(keys[e.code] !== undefined){
keys[e.code] = event.type === "keydown" ;
e.preventDefault();
}
},
}
addEventListener("keyup",keys.event);
addEventListener("keydown",keys.event);
focus();
const ships = {
items : [],
controling : 0,
add(ship){ this.items.push(ship) },
update(){
var i;
for(i = 0; i < this.items.length; i++){
if(keys["Digit" + (i+1)]){
if(this.controling !== -1){
this.items[this.controling].element.style.color = "green";
this.items[this.controling].hasControl = false;
}
this.controling = i;
this.items[i].element.style.color = "red";
this.items[i].hasControl = true;
}
this.items[i].updateUserIO();
this.items[i].updatePos();
}
}
}
const ship = {
element : null,
hasControl : false,
speed : 0,
speedC : 0, // chase value for speed limit mode
speedR : 0, // real value (real as in actual speed)
angle : 0,
angleC : 0, // as above
angleR : 0,
engSpeed : 0,
engSpeedC : 0,
engSpeedR : 0,
displayAngle : 0, // the display angle
deltaAngle : 0,
matrix : null, // matrix to create when instantiated
pos : null, // position of ship to create when instantiated
delta : null, // movement of ship to create when instantiated
checkInView(){
var bounds = this.element.getBoundingClientRect();
if(Math.max(bounds.right,bounds.left) < 0 && this.delta.x < 0){
this.pos.x = innerWidth;
}else if(Math.min(bounds.right,bounds.left) > innerWidth && this.delta.x > 0){
this.pos.x = 0;
}
if(Math.max(bounds.top,bounds.bottom) < 0 && this.delta.y < 0){
this.pos.y = innerHeight;
}else if( Math.min(bounds.top,bounds.bottom) > innerHeight && this.delta.y > 0){
this.pos.y = 0;
}
},
controls : {
oldSchool(){
if(this.hasControl){
if(keys.ArrowUp){
this.delta.x += Math.cos(this.angle) * 0.1;
this.delta.y += Math.sin(this.angle) * 0.1;
}
if(keys.ArrowLeft){
this.deltaAngle -= 0.001;
}
if(keys.ArrowRight){
this.deltaAngle += 0.001;
}
}
this.pos.x += this.delta.x;
this.pos.y += this.delta.y;
this.angle += this.deltaAngle;
this.displayAngle = this.angle;
this.delta.x *= 0.995;
this.delta.y *= 0.995;
this.deltaAngle *= 0.995;
},
oldSchoolDrag(){
if(this.hasControl){
if(keys.ArrowUp){
this.delta.x += Math.cos(this.angle) * 0.5;
this.delta.y += Math.sin(this.angle) * 0.5;
}
if(keys.ArrowLeft){
this.deltaAngle -= 0.01;
}
if(keys.ArrowRight){
this.deltaAngle += 0.01;
}
}
this.pos.x += this.delta.x;
this.pos.y += this.delta.y;
this.angle += this.deltaAngle;
this.delta.x *= 0.95;
this.delta.y *= 0.95;
this.deltaAngle *= 0.9;
this.displayAngle = this.angle;
},
speedster(){
if(this.hasControl){
if(keys.ArrowUp){
this.speed += 0.02;
}
if(keys.ArrowLeft){
this.deltaAngle -= 0.01;
}
if(keys.ArrowRight){
this.deltaAngle += 0.01;
}
}
this.speed *= 0.99;
this.deltaAngle *= 0.9;
this.angle += this.deltaAngle;
this.delta.x += Math.cos(this.angle) * this.speed;
this.delta.y += Math.sin(this.angle) * this.speed;
this.delta.x *= 0.95;
this.delta.y *= 0.95;
this.pos.x += this.delta.x;
this.pos.y += this.delta.y;
this.displayAngle = this.angle;
},
engineRev(){ // this one has a 3 control. Engine speed then affects acceleration.
if(this.hasControl){
if(keys.ArrowUp){
this.engSpeed = 3
}else{
this.engSpeed *= 0.9;
}
if(keys.ArrowLeft){
this.angle -= 0.1;
}
if(keys.ArrowRight){
this.angle += 0.1;
}
}else{
this.engSpeed *= 0.9;
}
this.engSpeedC += (this.engSpeed- this.engSpeedR) * 0.05;
this.engSpeedC *= 0.1;
this.engSpeedR += this.engSpeedC;
this.speedC += (this.engSpeedR - this.speedR) * 0.1;
this.speedC *= 0.4;
this.speedR += this.speedC;
this.angleC += (this.angle - this.angleR) * 0.1;
this.angleC *= 0.4;
this.angleR += this.angleC;
this.delta.x += Math.cos(this.angleR) * this.speedR * 0.1; // 0.1 reducing this as easier to manage speeds when values near pixel size and not 0.00umpteen0001
this.delta.y += Math.sin(this.angleR) * this.speedR * 0.1;
this.delta.x *= 0.99;
this.delta.y *= 0.99;
this.pos.x += this.delta.x;
this.pos.y += this.delta.y;
this.displayAngle = this.angleR;
},
speedLimiter(){
if(this.hasControl){
if(keys.ArrowUp){
this.speed = 15;
}else{
this.speed = 0;
}
if(keys.ArrowLeft){
this.angle -= 0.1;
}
if(keys.ArrowRight){
this.angle += 0.1;
}
}else{
this.speed = 0;
}
this.speedC += (this.speed - this.speedR) * 0.1;
this.speedC *= 0.4;
this.speedR += this.speedC;
this.angleC += (this.angle - this.angleR) * 0.1;
this.angleC *= 0.4;
this.angleR += this.angleC;
this.delta.x = Math.cos(this.angleR) * this.speedR;
this.delta.y = Math.sin(this.angleR) * this.speedR;
this.pos.x += this.delta.x;
this.pos.y += this.delta.y;
this.displayAngle = this.angleR;
}
},
updateUserIO(){
},
updatePos(){
this.checkInView();
var m = this.matrix;
m[3] = m[0] = Math.cos(this.displayAngle);
m[2] = -(m[1] = Math.sin(this.displayAngle));
m[4] = this.pos.x;
m[5] = this.pos.y;
this.element.style.transform = `matrix(${m.join(",")})`;
},
create(shape,container,xOff,yourRide){ // shape is a string
this.element = document.createElement("div")
this.element.style.position = "absolute";
this.element.style.top = this.element.style.left = "0px";
this.element.style.fontSize = "24px";
this.element.textContent = shape;
this.element.style.color = "green";
this.element.style.zIndex = 100;
container.appendChild(this.element);
this.matrix = [1,0,0,1,0,0];
this.pos = { x : innerWidth / 2 + innerWidth * xOff, y : innerHeight / 2 };
this.delta = { x : 0, y : 0};
this.updateUserIO = this.controls[yourRide];
return this;
}
}
var contain = document.createElement("div");
contain.style.position = "absolute";
contain.style.top = contain.style.left = "0px";
contain.style.width = contain.style.height = "100%";
contain.style.overflow = "hidden";
document.body.appendChild(contain);
window.focus();
ships.add(Object.assign({},ship).create("=Scl>",contain,-0.4,"oldSchool"));
ships.add(Object.assign({},ship).create("=Drg>",contain,-0.25,"oldSchoolDrag"));
ships.add(Object.assign({},ship).create("=Fast>",contain,-0.1,"speedster"));
ships.add(Object.assign({},ship).create("=Nimble>",contain,0.05,"speedLimiter"));
ships.add(Object.assign({},ship).create("=Rev>",contain,0.2,"engineRev"));
function mainLoop(){
ships.update();
requestAnimationFrame(mainLoop);
}
body {
font-family : verdana;
background : black;
color : #0F0;
}
Click to focus then keys 1, 2, 3, 4, 5 selects a ship. Arrow keys to fly. Best full page.
无数种变体
还有许多其他变体和方式。我喜欢使用二阶导数(来自 x += dx 的一阶导数 dx/dt(dt 是时间),二阶 de/dt 表示发动机功率)来模拟发动机的功率上升和下降可以给人一种很好的感觉。基本上它的
x += dx;
dx += de;
dx *= 0.999;
de *= 0.99;
if(input){ de += 0.01 }
什么适合你的游戏取决于你,你不必遵守规则,所以尝试不同的价值观和方法,直到你满意为止。