Вращение фигуры.
Возможно, больше, чем вы просили, но вы допускаете некоторые распространенные ошибки в вашем коде, которые негативно влияют на окончательный код и отнимают часть удовольствия от написания игры
Определение формы.
Локальное пространство
Если у вас есть вращающаяся, движущаяся и, возможно, масштабирующая (масштабирующая) форма, лучше всего определять ее форму с центром в ее собственном начале (локальном). пространство) (как указано в комментариях), так что преобразование его для отображения на холсте (мировое пространство) не требует сложности перемещения его в локальное пространство и обратно, если вы создаете объект в мировых координатах.
Предварительно определить путь
Вместо того, чтобы создавать путь кораблей каждый раз, когда вы его визуализируете, используйте Path2D для определения формы. Это позволяет избежать некоторых накладных расходов на создание пути путем перемещения вычислений в автозагрузку.
Ориентация
Естественная ориентация (вперед) преобразования холста происходит вдоль оси X. При построении объекта, который движется в мировом пространстве, лучше всего иметь переднюю точку вдоль одной оси. У вас есть корабль, направленный вверх по оси y в отрицательном направлении.
Очень распространенная ошибка - думать, что ctx.closePath
сроднидо ctx.beginPath
. closePath
не имеет ничего общего с beginPath
, оно больше похоже на lineTo
и создает дополнительную строку от последней точки пути к предыдущему moveTo
Пример
Кодопределяет корабль как двухмерный путь, фронт которого направлен вдоль оси x.
const shipShape = (() => {
const width = 20, height = 20;
const ship = new Path2D()
ship.lineTo(-width / 2, -height / 2);
ship.lineTo(width / 2, 0);
ship.lineTo(-width / 2, height / 2);
ship.closePath();
return ship;
})();
Сложность кода
Вы работаете над слишком сложным кодом. По мере роста вашей игры эта сложность будет усложнять внесение изменений и исправление ошибок. Всегда старайтесь, чтобы это было как можно проще.
Преобразование объекта
Существует множество способов преобразования объекта для визуализации. Наиболее распространенный способ, как вы это сделали. Однако этот метод требует много изменений состояния GPU (обмен данными между CPU и GPU). Изменение состояния может быть очень медленным (особенно на младших устройствах)
Следующий фрагмент вашего кода отмечает изменения состояния
ctx.save();
// the ctx.stroke in the following function is a state change
drawTriangle(ship_points[2], ship_points[1], ship_points[0],"white");
// All 3 of the following calls are state changes.
ctx.translate(ship_center.x, ship_center.y);
ctx.rotate(Math.PI/180 * angle);
ctx.restore(); // Depending on the saved state this can be very time expensive
Худшее изменение состояния - ctx.restore
, который сильно зависит от сохраненного состояния и изменений, внесенных в состояние между сохранением и восстановлением. Вы должны избегать использования сохранения и восстановления любой ценой, если вам нужен код производительности.
Визуализация с двумя состояниями
В следующем примере будет отображена двухмерная фигура с минимальным числом возможных изменений состояния и самым быстрымспособ визуализации преобразованного контента с использованием 2D API. Тем не менее, он оставляет состояние как есть, поэтому вы должны знать об этом при последующих визуализациях. Более эффективно полностью определять каждое состояние по мере необходимости, а не использовать сохранение и восстановление.
Примечание Я добавил масштаб, поскольку вам может понадобиться некоторое время.
function strokeShape(shape, pos, rotate = 0, scale = 1, style = ctx.strokeStyle) {
const xAx = Math.cos(rotate) * scale;
const xAy = Math.sin(rotate) * scale;
ctx.setTransform(xAx, xAy, -xAy, xAx, pos.x, pos.y); // set rotate scale and position
// in one state change
ctx.strokeStyle = style;
ctx.stroke(shape);
}
Чтобы нарисовать корабль, тогдапросто нужна строка
strokeShape(shipShape, {x:450, y:300}, rotate, 1, "white");
Демо
Собрав все вместе, мы получим следующее.
- Использование
requestAnimationFrame
для анимации (Никогда не используйте setInterval
) - Универсальная функция для создания пути
Path2D
из набора точек - Определение корабля как объекта для организации данных
ОБНОВЛЕНИЕ
Заметил ваш второй вопрос относительно движения. Когда на вопрос был дан ответ, я хотя бы немного расширил эту демонстрацию, чтобы дать подсказки о том, как двигать корабль, и о некоторых других играх, связанных с игрой. Нажмите, чтобы начать До тяги, левый правый повороты.
var started = false;
canvas.addEventListener("click",() => {
if (!started) {
requestAnimationFrame(updateFrame);
started = true;
}
})
const ctx = canvas.getContext("2d", {aplha:false});// aplha:false to avoid unneeded composition
ctx.font = "16px arial";
ctx.textAlign = "center";
fillBackground();
ctx.fillStyle = "white"
ctx.fillText("Click to Start", ctx.canvas.width / 2, ctx.canvas.height / 2);
document.addEventListener("keydown", keyboardEvent);
document.addEventListener("keyup", keyboardEvent);
const keys = {ArrowUp: false, ArrowLeft: false, ArrowRight: false}
function keyboardEvent(event) {
if(keys[event.code] !== undefined) {
event.preventDefault();
keys[event.code] = event.type === "keydown";
}
}
const width = 20, height = 20;
const TURN_RATE = 0.01; // in radians
const MAX_TURN_RATE = 0.1; // in radians
const REACTOR_WINDUP_RATE = 0.01; // in power units per frame
const REACTOR_MAX_POWER = 0.1; // in pixels per frame (frame = 1/60th sec)
const SPACE_QUANTUM_FLUX = 0.015; // drains ship moment per frame
const DEFLUXING_CONVERTER = 0.8; // How dirty the thruster is
const SHIP_HULL = [-width*(1/3), -height/2, width*(2/3), 0, -width*(1/3), height/2,"close"];
const SHIP_PORT = [width*(1/6), -height/8, width*(1/3), 0, width*(1/6), height/8,"close"];
const thrustParticlePool = [];
const thrustParticle = {
get pos() { return {x:0, y:0} },
get vel() { return {x:0, y:0} },
shape: createPath([-0.5,0,0.5,0]),
style: "#FFF",
rotate: 0,
pool: thrustParticlePool,
update() {
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
this.vel.x *= 0.996;
this.vel.y *= 0.996;
this.life -= 1;
},
init(x,y,direction, speed) {
const offCenter = Math.random()**2 * (Math.random() < 0.5 ? -1 : 1);
const offCenterA = Math.random()**2 * (Math.random() < 0.5 ? -1 : 1);
speed += speed * offCenterA;
speed **= 2.5;
this.pos.x = x + Math.cos(direction) * width * (2/3) - Math.sin(direction) * height * (1/6) * offCenter;
this.pos.y = y + Math.sin(direction) * width * (2/3) + Math.cos(direction) * height * (1/6) * offCenter;
direction += direction * 0.1 * offCenter;
this.rotate = direction;
this.vel.x = Math.cos(direction) * speed;
this.vel.y = Math.sin(direction) * speed;
this.life = 100;
},
};
const particles = Object.assign([],{
add(type,...args) {
var p;
if(type.pool.length) {
p = type.pool.pop();
} else {
p = Object.assign({}, type);
}
p.init(...args);
this.push(p);
},
updateDraw() {
var i = 0
while(i < this.length) {
const p = this[i];
p.update();
if (p.life <= 0) {
this.splice(i--,1)[0];
if (p.pool) { p.pool.push(p) }
} else {
strokeShape(p.shape, p.pos, p.rotate, 1, p.style);
}
i++;
}
}
});
function createPath(...paths) {
var i, path = new Path2D;
for(const points of paths) {
i = 0;
path.moveTo(points[i++],points[i++])
while (i < points.length -1) { path.lineTo(points[i++],points[i++]) }
points[i] === "close" && path.closePath();
}
return path;
}
const ship = {
shapes: {
normal: createPath(SHIP_HULL, SHIP_PORT),
thrustingA: createPath(SHIP_HULL, SHIP_PORT,
[-width*(1/3), -height/4, -width*(1/3)-height/4,0, -width*(1/3), height/4]
),
thrustingB: createPath(SHIP_HULL, SHIP_PORT,
[-width*(1/3), -height/3.5, -width*(1/3)-height/2.4,0, -width*(1/3), height/3.5]
),
},
shape: null,
rotate: 0, // point left to right along x axis
deltaRotate: 0,
pos: {x : 200, y: 100},
vel: {x : 0, y: 0},
power: 0,
style: "#FFF", // named colours take about 10% longer to set than Hex colours
update() {
if (keys.ArrowUp) {
this.shape = this.shapes.thrustingA === this.shape ? this.shapes.thrustingB : this.shapes.thrustingA;
this.power = this.power < REACTOR_MAX_POWER ? this.power + REACTOR_WINDUP_RATE : REACTOR_MAX_POWER;
if (Math.random() < DEFLUXING_CONVERTER) {
particles.add(
thrustParticle,
this.pos.x, this.pos.y,
this.rotate + Math.PI,
this.power * 8,
);
}
} else {
this.shape = this.shapes.normal;
this.power = 0;
}
var dr = this.deltaRotate;
dr *= 0.95;
dr = keys.ArrowLeft ? dr - TURN_RATE : dr;
dr = keys.ArrowRight ? dr + TURN_RATE : dr;
dr = Math.abs(dr) > MAX_TURN_RATE ? MAX_TURN_RATE * Math.sign(dr) : dr;
this.rotate += (this.deltaRotate = dr);
this.vel.x += Math.cos(this.rotate) * this.power;
this.vel.y += Math.sin(this.rotate) * this.power;
const speed = (this.vel.x * this.vel.x + this.vel.y * this.vel.y)**4;
if (speed > 0.0) {
this.vel.x = this.vel.x * (speed / (speed * (1+SPACE_QUANTUM_FLUX)));
this.vel.y = this.vel.y * (speed / (speed * (1+SPACE_QUANTUM_FLUX)));
}
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
this.pos.x = (this.pos.x + ctx.canvas.width * 2) % ctx.canvas.width;
this.pos.y = (this.pos.y + ctx.canvas.height * 2) % ctx.canvas.height;
},
draw() {
strokeShape(ship.shape, ship.pos, ship.rotate, 1, ship.style);
}
};
function strokeShape(shape, pos, rotate = 0, scale = 1, style = ctx.strokeStyle) {
const xAx = Math.cos(rotate) * scale; // direction and size of the top of a
const xAy = Math.sin(rotate) * scale; // single pixel
ctx.setTransform(xAx, xAy, -xAy, xAx, pos.x, pos.y); // one state change
ctx.strokeStyle = style;
ctx.stroke(shape);
}
function fillBackground() {
ctx.fillStyle = "#000";
ctx.setTransform(1,0,0,1,0,0); //ensure that the GPU Transform state is correct
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function updateFrame(time) {
fillBackground();
ship.update();
particles.updateDraw();
ship.draw();
requestAnimationFrame(updateFrame);
}
<canvas id="canvas" width="400" height="200"></canvas>