Главная » Игры » Как я строил гексапод в Space Engineers. Часть 1

Как я строил гексапод в Space Engineers. Часть 1

Вектор:

class Vector ; distanceTo(vector) { return Math.sqrt(Math.pow(this.x - vector.x, 2) + Math.pow(this.y - vector.y, 2) + Math.pow(this.z - vector.z, 2)); } diff(vector) { return new Vector( this.x - vector.x, this.y - vector.y, this.z - vector.z ); } add(vector) { return new Vector( this.x + vector.x, this.y + vector.y, this.z + vector.z ); } }

Сустав:

class Joint { constructor(angle, position, length) { this.angle = angle; this.position = position; this.length = length; this.targetAngle = angle; this.previousAngle = angle; this.velocity = 0; }; setTargetAngle(targetAngle) { this.targetAngle = targetAngle; this.velocity = this.targetAngle - this.normalizeAngle(this.angle); } normalizeAngle(angle) { while (angle <= -Math.PI) angle += Math.PI * 2; while (angle > Math.PI) angle -= Math.PI * 2; return angle; } getCurrentVelocity() {//per tick return this.normalizeAngle(this.angle - this.previousAngle); } tick() { this.previousAngle = this.angle; this.angle = this.angle + this.velocity; } }

Шаг — структура данных для управления ногой:

class Step { constructor( idlePosition,//vector relative to inner joint angle,//step direction length,//step length height,//step height phaseShift// ) { this.idlePosition = idlePosition; this.angle = angle;//radians this.length = length; this.height = height; this.phaseShift = phaseShift; } }

Нога:

class Leg { constructor( vehicleCenter, innerJoint, midJoint, outerJoint, step, phaseStep ) { this.vehicleCenter = vehicleCenter; this.innerJoint = innerJoint; this.midJoint = midJoint; this.outerJoint = outerJoint; this.step = step; this.phaseStep = phaseStep; this.innerJoint.length = innerJoint.position.distanceTo(midJoint.position);//calculate this.midJoint.length = midJoint.position.distanceTo(outerJoint.position);//calculate //this.outerJoint.length = 100; this.joints = [innerJoint, midJoint, outerJoint]; this.preCalculateAngles(); } preCalculateAngles() { this.angles = {}; for (let phase = 0; phase < 360; phase += this.phaseStep) { this.angles[phase] = this.getLegAngles(this.getEsimatedLegPosition(phase, this.step.phaseShift)) } } applyStepHeight(z) { const idleYawRad = Math.atan2(this.step.idlePosition.x, this.step.idlePosition.y); const diffHypot = Math.hypot(this.step.idlePosition.x, this.step.idlePosition.y); const minZ = Math.abs(this.midJoint.length - this.outerJoint.length); const maxZ = (this.midJoint.length + this.outerJoint.length) * 0.6; if (Math.hypot(z, 0) > maxZ) { z = z > 0 ? maxZ : -maxZ; } const safeY = (this.innerJoint.length + this.midJoint.length * 0.5 + this.outerJoint.length * 0.5) * Math.cos(idleYawRad); const vAngle = Math.asin(z / safeY); const y = safeY * Math.cos(vAngle) * Math.cos(idleYawRad); this.step.idlePosition.z = z; this.step.idlePosition.y = this.step.idlePosition.y > 0 ? y : -y; this.preCalculateAngles(); } applyStepAngle(angle) { this.step.angle = angle; this.preCalculateAngles(); } applyPhase(phase/*0-360*/) { const legAngles = this.angles[phase]; this.innerJoint.setTargetAngle(legAngles.yaw); this.midJoint.setTargetAngle(legAngles.midPitch); this.outerJoint.setTargetAngle(legAngles.outerPitch); } getEsimatedLegPosition(phase, phaseShift) { phase = (phase + phaseShift) % 360; const stepX = ((phase < 180 ? phase : 180 - phase % 180) / 180 - 0.5) * this.step.length;//linear movement along step direction const stepZ = Math.max(Math.sin(phase * Math.PI / 180), -0.2) * this.step.height / 1.2; //const stepZ = Math.max((phase > 180 ? Math.cos(phase * Math.PI / 360) + 0.9 : Math.cos((phase - 120) * Math.PI / 360)) * .9 - .1, 0) * this.step.height; const x = this.step.idlePosition.x + stepX * Math.cos(this.step.angle); const y = this.step.idlePosition.y + stepX * Math.sin(this.step.angle); return new Vector(x, y, stepZ); } getLegAngles(esimatedLegPosition) { const yawRad = Math.atan2(esimatedLegPosition.x, esimatedLegPosition.y); const dx = Math.hypot(esimatedLegPosition.x, esimatedLegPosition.y) - this.innerJoint.length; const dz = this.step.idlePosition.z + esimatedLegPosition.z; const hyp = Math.hypot(dx, dz); if (hyp > this.midJoint.length + this.outerJoint.length) {//out of reach hyp = this.midJoint.length + this.outerJoint.length; } const innerAngleRad = Math.acos((this.outerJoint.length * this.outerJoint.length - this.midJoint.length * this.midJoint.length - hyp * hyp) / (-2 * this.midJoint.length * hyp)) + Math.atan2(dz, dx); const outerAngleRad = Math.acos((hyp * hyp - this.midJoint.length * this.midJoint.length - this.outerJoint.length * this.outerJoint.length) / (-2 * this.midJoint.length * this.outerJoint.length)) - Math.PI; if (isNaN(yawRad) || isNaN(innerAngleRad) || isNaN(outerAngleRad)) { console.log(yawRad, innerAngleRad, outerAngleRad); console.log(dx, dz); return; } return { yaw: yawRad, midPitch: innerAngleRad, outerPitch: outerAngleRad }; } getMaxMinAngles() { const angles = [0, 90, 180, 270].map((phase) => { return this.getLegAngles(getEsimatedLegPosition(phase, 0)); }); return { yawMin: Math.min(angles.map((x) => { return x.yaw })), yawMax: Math.max(angles.map((x) => { return x.yaw })), midPitchMin: Math.min(angles.map((x) => { return x.midPitch })), midPitchMax: Math.max(angles.map((x) => { return x.midPitch })), outerPitchMin: Math.min(angles.map((x) => { return x.outerPitch })), outerPitchMax: Math.max(angles.map((x) => { return x.outerPitch })), } } tick() { this.joints.forEach(function (joint) { joint.tick(); }); } getVectors() { const res = []; const sinYaw = Math.sin(this.innerJoint.angle); const cosYaw = Math.cos(this.innerJoint.angle); let currentVector = this.vehicleCenter; res.push(currentVector); currentVector = currentVector.add(this.innerJoint.position); res.push(currentVector); currentVector = currentVector.add(new Vector( this.innerJoint.length * sinYaw, this.innerJoint.length * cosYaw, 0 )); res.push(currentVector); const dxMid = Math.cos(this.midJoint.angle) * this.midJoint.length; const dzMid = Math.sin(this.midJoint.angle) * this.midJoint.length; currentVector = currentVector.add(new Vector( dxMid * sinYaw, dxMid * cosYaw, dzMid )); res.push(currentVector); const c = this.midJoint.angle + this.outerJoint.angle; const dxOuter = Math.cos(c) * this.outerJoint.length; const dzOuter = Math.sin(c) * this.outerJoint.length; currentVector = currentVector.add(new Vector( dxOuter * sinYaw, dxOuter * cosYaw, dzOuter )); res.push(currentVector); return res; } }

Робот:

class Hexapod { constructor(phaseStep) { this.idleHeight = -70; this.stepAngle = 0; this.turnAngle = 0; this.stepLength = 70; this.stepHeight = 30; this.debugPoints = []; const vehicleCenter = new Vector(0, 0, 0); this.legs = [ new Leg( vehicleCenter, new Joint(0, new Vector(-70, 10, 0), 50), new Joint(0, new Vector(-70, 60, 0), 50), new Joint(0, new Vector(-70, 110, 0), 70), new Step(new Vector(-30, 90, this.idleHeight), this.stepAngle, this.stepLength, this.stepHeight, 0), phaseStep ), new Leg( vehicleCenter, new Joint(0, new Vector(-70, -10, 0), 50), new Joint(0, new Vector(-70, -60, 0), 50), new Joint(0, new Vector(-70, -110, 0), 70), new Step(new Vector(-30, -90, this.idleHeight), this.stepAngle, this.stepLength, this.stepHeight, 180), phaseStep ), new Leg( vehicleCenter, new Joint(0, new Vector(0, 10, 0), 50), new Joint(0, new Vector(0, 60, 0), 50), new Joint(0, new Vector(0, 110, 0), 70), new Step(new Vector(0, 100, this.idleHeight), this.stepAngle, this.stepLength, this.stepHeight, 180), phaseStep ), new Leg( vehicleCenter, new Joint(0, new Vector(0, -10, 0), 50), new Joint(0, new Vector(0, -60, 0), 50), new Joint(0, new Vector(0, -110, 0), 70), new Step(new Vector(0, -100, this.idleHeight), this.stepAngle, this.stepLength, this.stepHeight, 0), phaseStep ), new Leg( vehicleCenter, new Joint(0, new Vector(70, 10, 0), 50), new Joint(0, new Vector(70, 60, 0), 50), new Joint(0, new Vector(70, 110, 0), 70), new Step(new Vector(30, 90, this.idleHeight), this.stepAngle, this.stepLength, this.stepHeight, 0), phaseStep ), new Leg( vehicleCenter, new Joint(0, new Vector(70, -10, 0), 50), new Joint(0, new Vector(70, -60, 0), 50), new Joint(0, new Vector(70, -110, 0), 70), new Step(new Vector(30, -90, this.idleHeight), this.stepAngle, this.stepLength, this.stepHeight, 180), phaseStep ), ]; } applyPhase(phase/*0-360*/) { this.legs.forEach(function (leg) { leg.applyPhase(phase); }); } changeHeight(value) { this.legs.forEach(function (leg) { leg.applyStepHeight(this.idleHeight + value); }, this); } changeStepLength(value) { this.stepLength += value; this.legs.forEach(function (leg) { leg.step.length = this.stepLength; leg.preCalculateAngles(); }, this); } applyTurn1(centerX, centerY) { const angleToAxis = Math.atan2(centerX, centerY); const distanceToAxis = Math.hypot(centerX, centerY); distanceToAxis = 1000/distanceToAxis; this.legs.forEach(leg => { const dx = leg.step.idlePosition.x + leg.innerJoint.position.x + Math.sin(angleToAxis)*distanceToAxis || 0; const dy = leg.step.idlePosition.y + leg.innerJoint.position.y + Math.cos(angleToAxis)*distanceToAxis || 0; const angle = Math.atan2(dy,dx); const hypIdle = Math.hypot(dx, dy); leg.applyStepAngle(angle+Math.PI/2); leg.step.length = this.stepLength *hypIdle/ ((distanceToAxis || 0) + 1000); }); } applyTurn(centerX, centerY) { this.stepAngle = Math.atan2(centerX, centerY); if (this.stepAngle > Math.PI / 2) this.stepAngle -= Math.PI; if (this.stepAngle < -Math.PI / 2) this.stepAngle += Math.PI; const mults = this.legs.map(leg => Math.hypot(leg.step.idlePosition.y + leg.innerJoint.position.y, leg.step.idlePosition.x + leg.innerJoint.position.x) / Math.hypot(leg.step.idlePosition.y + leg.innerJoint.position.y + centerY*.3, leg.step.idlePosition.x + leg.innerJoint.position.x + centerX*.3)); const minMult = Math.min(...mults); const maxMult = Math.max(...mults); const mult = minMult / maxMult; const d = Math.pow(Math.max(...this.legs.map(leg =>Math.hypot(leg.step.idlePosition.y + leg.innerJoint.position.y, leg.step.idlePosition.x + leg.innerJoint.position.x))),2)/Math.hypot(centerX,centerY); const a = Math.atan2(centerX,centerY); this.legs.forEach(leg => { const dx = leg.step.idlePosition.x + leg.innerJoint.position.x; const dy = leg.step.idlePosition.y + leg.innerJoint.position.y; const idleAngle = Math.atan2(dx, dy) + this.stepAngle; const turnAngle = Math.atan2(dx + centerX, dy + centerY); const hypIdle = Math.hypot(dx, dy); const hyp = Math.hypot(dx + centerX, dy + centerY); leg.applyStepAngle(turnAngle - idleAngle); leg.step.length = this.stepLength * hyp / hypIdle * mult; }); this.debugPoints = [new Vector(Math.sin(a)*-d,Math.cos(a)*-d,0)]; } tick() { this.legs.forEach(function (leg) { leg.tick(); }); } getVectors() { return this.legs.map(function (leg) { return leg.getVectors() }); } }

Но для отрисовки понадобятся еще несколько классов:

Обертка над Canvas:

class Canvas { constructor(id, label, axisSelectorX, axisSelectorY) { const self = this; this.id = id; this.label = label; this.canvas = document.getElementById(id); this.ctx = this.canvas.getContext('2d'); this.axisSelectorX = axisSelectorX; this.axisSelectorY = axisSelectorY; this.canvasHeight = this.canvas.offsetHeight; this.canvasWidth = this.canvas.offsetWidth; this.initialY = this.canvasHeight / 2; this.initialX = this.canvasWidth / 2; this.traceCounter = 0; this.maxTraces = 50; this.traces = {}; const axisSize = 150; this.axisVectors = [ [ new Vector(-axisSize, -axisSize, -axisSize), new Vector(-axisSize, -axisSize, axisSize) ], [ new Vector(-axisSize, -axisSize, -axisSize), new Vector(-axisSize, axisSize, -axisSize) ], [ new Vector(-axisSize, -axisSize, -axisSize), new Vector(axisSize, -axisSize, -axisSize) ], ] this.mouseOver = false; this.mousePos = { x: 0, y: 0 };//relative to center this.clickPos = { x: 0, y: 0 };//relative to center this.canvas.addEventListener("mouseenter", function (event) { self.mouseOver = true; }, false); this.canvas.addEventListener("mouseleave", function (event) { self.mouseOver = false; }, false); this.canvas.addEventListener("mousemove", function (event) { if (self.mouseOver) { self.mousePos = { x: event.offsetX - self.initialX, y: event.offsetY - self.initialY }; } }, false); this.canvas.addEventListener("mouseup", function (event) { if (self.mouseOver) { self.clickPos = { x: event.offsetX - self.initialX, y: event.offsetY - self.initialY }; } }, false); }; clear(drawAxis) { this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight); this.ctx.strokeStyle = "#000000"; this.ctx.strokeText(this.label, 10, 10); if (drawAxis) { this.axisVectors.forEach(function (vectors, i) { this.ctx.moveTo(this.initialX, this.initialY); this.ctx.beginPath(); vectors.forEach(function (vector) { this.ctx.lineTo(this.initialX + this.axisSelectorX(vector), this.initialY - this.axisSelectorY(vector)); }, this); this.ctx.stroke(); const lastVector = vectors[vectors.length - 1]; this.traces[[this.traceCounter, i]] = lastVector }, this); } } drawVectors(vectors) {/*2d array*/ vectors.forEach(function (vectors, i) { this.ctx.moveTo(this.initialX, this.initialY); this.ctx.beginPath(); vectors.forEach(function (vector) { this.ctx.lineTo(this.initialX + this.axisSelectorX(vector), this.initialY - this.axisSelectorY(vector)); }, this); this.ctx.stroke(); const lastVector = vectors[vectors.length - 1]; this.traces[[this.traceCounter, i]] = lastVector }, this); for (const key in this.traces) { const vector = this.traces[key]; this.ctx.fillStyle = "#FF0000";//red this.ctx.fillRect(this.initialX + this.axisSelectorX(vector), this.initialY - this.axisSelectorY(vector), 1, 1); } this.ctx.strokeStyle = "#000000"; this.ctx.beginPath(); this.ctx.arc(this.clickPos.x + this.initialX, this.clickPos.y + this.initialY, 5, 0, 2 * Math.PI); this.ctx.stroke(); if (this.mouseOver) { this.ctx.strokeStyle = "#00FF00"; this.ctx.beginPath(); this.ctx.arc(this.mousePos.x + this.initialX, this.mousePos.y + this.initialY, 10, 0, 2 * Math.PI); this.ctx.stroke(); } this.traceCounter = (this.traceCounter + 1) % this.maxTraces; } drawPoints(points) { this.ctx.fillStyle = "#00ff00";//green points.forEach(function (point) { this.ctx.fillRect(this.initialX + this.axisSelectorX(point), this.initialY - this.axisSelectorY(point), 3, 3); }, this); } }

В классе Leg есть метод для получения текущих координат суставов. Вот эти координаты мы и будем отрисовывать.

Так-же я добавил отрисовку точек, в которых находилась нога в N последних тиков.

И наконец Worker, который будет запускать симуляцию:

class Worker { constructor(tickTime) { const self = this; this.phaseStep = 5; this.tickTime = tickTime; const tan30 = Math.tan(Math.PI / 6); const scale = 0.7; this.canvases = [ new Canvas('canvasForward', 'yz Forward', function (v) { return v.y }, function (v) { return v.z }), new Canvas('canvasSide', 'xz Side', function (v) { return v.x }, function (v) { return v.z }), new Canvas('canvasTop', 'xy Top', function (v) { return v.x }, function (v) { return -v.y }), new Canvas('canvasIso', 'xyz Iso', function (v) { return v.x * scale + v.y * scale }, function (v) { return v.z * scale + v.x * tan30 * scale - v.y * tan30 * scale }), ]; this.bot = new Hexapod(this.phaseStep); this.phase = 0; this.focus = true; window.addEventListener('focus', function () { console.log('focus'); self.focus = true; }); window.addEventListener('blur', function () { console.log('blur'); self.focus = false; }); this.start(); } tick(argument) { const canvasForward = this.canvases[0]; const bot = this.bot; if (canvasForward.mouseOver) { bot.changeHeight(-canvasForward.mousePos.y); } else { bot.changeHeight(0); } const canvasTop = this.canvases[2]; if (canvasTop.mouseOver) { bot.applyTurn(-canvasTop.mousePos.x, -canvasTop.mousePos.y); } else { bot.applyTurn(0, 0); } this.phase = (this.phase + this.phaseStep) % 360; bot.applyPhase(this.phase); bot.tick(); const vectors = bot.getVectors(); this.canvases.forEach(function (c) { c.clear(false); c.drawVectors(vectors); c.drawPoints(bot.debugPoints); }); } start() { this.stop(); this.interval = setInterval((function (self) { return function () { if (self.focus) { self.tick(); } } })(this), this.tickTime); } stop() { clearInterval(this.interval); } }


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Руководство по JavaScript, часть 3: переменные, типы данных, выражения, объекты

Сегодня, в третьей части перевода руководства по JavaScript, мы поговорим о разных способах объявления переменных, о типах данных, о выражениях и об особенностях работы с объектами. → Часть 1: первая программа, особенности языка, стандарты→ Часть 2: стиль кода и структура ...

Триумф и трагедия «Бурана»

В полностью автоматическом режиме он совершил 2 витка вокруг Земли и успешно приземлился спустя 205 минут. Ровно 30 лет назад с космодрома Байконур на ракете-носителе «Энергия» в свой единственный полёт отправился корабль «Буран». Это стало несомненным триумфом советской космонавтики, впервые ...