Migrate Box2D to Planck.js in core game logic, items, debug draw, and menu. Remove legacy Box2D references, update level and item loading, and improve debug draw for Planck.

This commit is contained in:
Karl Pannek 2025-07-17 18:50:16 +02:00
parent 799601f24d
commit da6e9a244b
15 changed files with 66 additions and 201 deletions

16
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "npm start",
"runtimeExecutable": "npm",
"runtimeArgs": [
"start"
],
"port": 9229,
"console": "integratedTerminal"
}
]
}

View file

@ -51,18 +51,17 @@ function (Parent, Item, Box2D, nc, Assert) {
var ownVelocity = this.body.getLinearVelocity(); var ownVelocity = this.body.getLinearVelocity();
var b2Math = Box2D.Common.Math.b2Math; var absItemVelocity = { x: Math.abs(itemVelocity.x), y: Math.abs(itemVelocity.y) };
var absItemVelocity = b2Math.AbsV(itemVelocity);
var min = 1; var min = 1;
var damage = 0; var damage = 0;
if(absItemVelocity.x > min || absItemVelocity.y > min) { if(absItemVelocity.x > min || absItemVelocity.y > min) {
if(item.lastMoved && item.lastMoved.player != this.player) { if(item.lastMoved && item.lastMoved.player != this.player) {
var collision = b2Math.SubtractVV(itemVelocity, ownVelocity); var collision = planck.Vec2(itemVelocity).sub(planck.Vec2(ownVelocity));
// Tested max velocity banana: 50 // Tested max velocity banana: 50
var velocityDamage = collision.Length() / 50; var velocityDamage = collision.length() / 50;
// Max weight of piano: 15 // Max weight of piano: 15
var weightDamage = item.options.weight / 15; var weightDamage = item.options.weight / 15;

View file

@ -115,6 +115,13 @@ function (Settings, nc, Screenfull, Graph, pointerLockManager) {
li.appendChild(this.ping); li.appendChild(this.ping);
this.devToolsContainer.appendChild(li); this.devToolsContainer.appendChild(li);
// Add 'planck' label in its own li next to fullscreen
li = document.createElement("li");
var planckLabel = document.createElement("label");
planckLabel.style.fontWeight = "bold";
planckLabel.textContent = "planck";
li.appendChild(planckLabel);
this.devToolsContainer.appendChild(li);
// create debug mode // create debug mode
li = document.createElement("li"); li = document.createElement("li");
@ -129,7 +136,6 @@ function (Settings, nc, Screenfull, Graph, pointerLockManager) {
li.appendChild(label); li.appendChild(label);
this.devToolsContainer.appendChild(li); this.devToolsContainer.appendChild(li);
// create Fullscreen // create Fullscreen
li = document.createElement("li"); li = document.createElement("li");
li.id = "fullscreen"; li.id = "fullscreen";

View file

@ -6,174 +6,13 @@ function (Box2D) {
"use strict"; "use strict";
var Parent = Box2D.Dynamics.b2DebugDraw; // Wrapper for PlanckDebugDraw
function DebugDraw(canvas) {
function DebugDraw() { // Use the PlanckDebugDraw implementation
Parent.call(this); var PlanckDebugDraw = require('./PlanckDebugDraw');
this.m_drawScale = 1; return new PlanckDebugDraw(canvas);
} }
DebugDraw.prototype = Object.create(Parent.prototype);
DebugDraw.prototype.setColor = function(color) {
this.m_ctx.debugColor = color.color;
this.m_ctx.debugFillAlpha = this.m_fillAlpha;
this.m_ctx.lineStyle(1, this.m_ctx.debugColor, this.m_alpha);
};
DebugDraw.prototype.SetSprite = function(sprite) {
this.m_ctx = sprite;
this.m_sprite = {
graphics: {
clear: function () {
sprite.clear();
sprite.lineStyle(1, 0xffffff, 0.8);
}
}
};
this.m_ctx.beginPath = function() {
this.beginFill(this.debugColor, this.debugFillAlpha);
};
this.m_ctx.closePath = function() {
this.endFill();
};
this.m_ctx.fill = function() {
this.endFill();
};
this.m_ctx.stroke = function() {
// do nothing
};
this.m_ctx.arc = function(x, y, radius, startingAngle, endingAngle, counterClockwise) {
this.drawCircle(x, y, radius);
}
};
DebugDraw.prototype.DrawPolygon = function (vertices, vertexCount, color) {
this.setColor(color);
Parent.prototype.DrawPolygon.call(this, arguments);
};
DebugDraw.prototype.DrawSolidPolygon = function (vertices, vertexCount, color) {
this.setColor(color);
Parent.prototype.DrawSolidPolygon.apply(this, arguments);
};
DebugDraw.prototype.DrawCircle = function (center, radius, color) {
this.setColor(color);
Parent.prototype.DrawCircle.apply(this, arguments);
};
DebugDraw.prototype.DrawSolidCircle = function (center, radius, axis, color) {
this.setColor(color);
Parent.prototype.DrawSolidCircle.apply(this, arguments);
};
DebugDraw.prototype.DrawSegment = function (p1, p2, color) {
this.setColor(color);
Parent.prototype.DrawSegment.apply(this, arguments);
};
DebugDraw.prototype.DrawTransform = function (xf) {
this.setColor(0xff0000);
Parent.prototype.DrawTransform.apply(this, arguments);
};
/*
DebugDraw.prototype.DrawPolygon = function (vertices, vertexCount, color) {
if (!vertexCount) return;
var s = this.m_ctx;
var drawScale = this.m_drawScale;
s.beginPath();
s.strokeStyle = this._color(color.color, this.m_alpha);
s.moveTo(vertices[0].x * drawScale, vertices[0].y * drawScale);
for (var i = 1; i < vertexCount; i++) {
s.lineTo(vertices[i].x * drawScale, vertices[i].y * drawScale);
}
s.lineTo(vertices[0].x * drawScale, vertices[0].y * drawScale);
s.closePath();
s.stroke();
};
DebugDraw.prototype.DrawSolidPolygon = function (vertices, vertexCount, color) {
if (!vertexCount) return;
var s = this.m_ctx;
var drawScale = this.m_drawScale;
s.beginPath();
s.strokeStyle = this._color(color.color, this.m_alpha);
s.fillStyle = this._color(color.color, this.m_fillAlpha);
s.moveTo(vertices[0].x * drawScale, vertices[0].y * drawScale);
for (var i = 1; i < vertexCount; i++) {
s.lineTo(vertices[i].x * drawScale, vertices[i].y * drawScale);
}
s.lineTo(vertices[0].x * drawScale, vertices[0].y * drawScale);
s.closePath();
s.fill();
s.stroke();
};
DebugDraw.prototype.DrawCircle = function (center, radius, color) {
if (!radius) return;
var s = this.m_ctx;
var drawScale = this.m_drawScale;
s.beginPath();
s.strokeStyle = this._color(color.color, this.m_alpha);
s.arc(center.x * drawScale, center.y * drawScale, radius * drawScale, 0, Math.PI * 2, true);
s.closePath();
s.stroke();
};
DebugDraw.prototype.DrawSolidCircle = function (center, radius, axis, color) {
if (!radius) return;
var s = this.m_ctx,
drawScale = this.m_drawScale,
cx = center.x * drawScale,
cy = center.y * drawScale;
s.moveTo(0, 0);
s.beginPath();
s.strokeStyle = this._color(color.color, this.m_alpha);
s.fillStyle = this._color(color.color, this.m_fillAlpha);
s.arc(cx, cy, radius * drawScale, 0, Math.PI * 2, true);
s.moveTo(cx, cy);
s.lineTo((center.x + axis.x * radius) * drawScale, (center.y + axis.y * radius) * drawScale);
s.closePath();
s.fill();
s.stroke();
};
DebugDraw.prototype.DrawSegment = function (p1, p2, color) {
var s = this.m_ctx,
drawScale = this.m_drawScale;
s.strokeStyle = this._color(color.color, this.m_alpha);
s.beginPath();
s.moveTo(p1.x * drawScale, p1.y * drawScale);
s.lineTo(p2.x * drawScale, p2.y * drawScale);
s.closePath();
s.stroke();
};
DebugDraw.prototype.DrawTransform = function (xf) {
var s = this.m_ctx,
drawScale = this.m_drawScale;
s.beginPath();
s.strokeStyle = this._color(0xff0000, this.m_alpha);
s.moveTo(xf.position.x * drawScale, xf.position.y * drawScale);
s.lineTo((xf.position.x + this.m_xformScale * xf.R.col1.x) * drawScale, (xf.position.y + this.m_xformScale * xf.R.col1.y) * drawScale);
s.strokeStyle = this._color(0xff00, this.m_alpha);
s.moveTo(xf.position.x * drawScale, xf.position.y * drawScale);
s.lineTo((xf.position.x + this.m_xformScale * xf.R.col2.x) * drawScale, (xf.position.y + this.m_xformScale * xf.R.col2.y) * drawScale);
s.closePath();
s.stroke();
};
*/
return DebugDraw; return DebugDraw;
}); });

View file

@ -91,7 +91,7 @@ function () {
CHANNEL_END_ROUND_TIME: 20, //10, CHANNEL_END_ROUND_TIME: 20, //10,
CHANNEL_DEFAULT_MAX_USERS: 10, CHANNEL_DEFAULT_MAX_USERS: 10,
CHANNEL_DEFAULT_SCORE_LIMIT: 5, CHANNEL_DEFAULT_SCORE_LIMIT: 5,
CHANNEL_DEFAULT_LEVELS: ["debug"], CHANNEL_DEFAULT_LEVELS: ["stones", "gangsta", "residence"],
CHANNEL_RECORD_SESSION: false, CHANNEL_RECORD_SESSION: false,
// ME STATE // ME STATE

View file

@ -117,7 +117,7 @@ function (Parent, Exception, planck, Settings, CollisionDetector, Item, nc, Asse
shape: planck.Box( shape: planck.Box(
r / 2 / R, r / 2 / R,
(h / 2 + r / 4) / R, (h / 2 + r / 4) / R,
planck.Vec2(sign * r / 2 / R, h / 2 / R) planck.Vec2(sign * r / 2 / R, 0)
), ),
density: 0, density: 0,
friction: 0, friction: 0,
@ -133,7 +133,7 @@ function (Parent, Exception, planck, Settings, CollisionDetector, Item, nc, Asse
// Area sensor // Area sensor
addFixture({ addFixture({
shape: planck.Box((w + a) / 2 / R, (h + a) / 2 / R, planck.Vec2(0, h / 2 / R)), shape: planck.Box((w + a) / 2 / R, (h + a) / 2 / R, planck.Vec2(0, 0)),
density: 0, density: 0,
friction: 0, friction: 0,
restitution: 0, restitution: 0,
@ -319,15 +319,20 @@ function (Parent, Exception, planck, Settings, CollisionDetector, Item, nc, Asse
Assert.number(this.lookDirection); Assert.number(this.lookDirection);
var handPosition = planck.Vec2( var handPosition = planck.Vec2(
bodyPosition.x + ((this.width / 2 / Settings.RATIO) * this.lookDirection), bodyPosition.x + ((this.width / 2 / Settings.RATIO) * this.lookDirection),
bodyPosition.y - this.height / 4 * 2 / Settings.RATIO // 2/3 of the body height bodyPosition.y - 0 / Settings.RATIO // 2/3 of the body height
); );
this.holdingItem.reposition(handPosition, this.lookDirection); this.holdingItem.reposition(handPosition, this.lookDirection);
var jointDef = new Box2D.Dynamics.Joints.b2WeldJointDef(); // Planck.js WeldJoint
jointDef.initialize(this.body, this.holdingItem.body, this.holdingItem.getGrabPoint()); var jointDef = {
bodyA: this.body,
this.holdingJoint = this.body.getWorld().createJoint(jointDef); bodyB: this.holdingItem.body,
localAnchorA: this.body.getLocalPoint(this.holdingItem.getGrabPoint()),
localAnchorB: this.holdingItem.body.getLocalPoint(this.holdingItem.getGrabPoint()),
referenceAngle: 0
};
this.holdingJoint = this.body.getWorld().createJoint(planck.WeldJoint(jointDef));
} }
}; };

View file

@ -8,7 +8,7 @@ define([
"Lib/Utilities/Assert" "Lib/Utilities/Assert"
], ],
function (Parent, Box2D, optionsHelper, Settings, Exception, nc, Assert) { function (Parent, planck, optionsHelper, Settings, Exception, nc, Assert) {
"use strict"; "use strict";
@ -35,10 +35,10 @@ function (Parent, Box2D, optionsHelper, Settings, Exception, nc, Assert) {
Parent.call(this, physicsEngine, uid); Parent.call(this, physicsEngine, uid);
this.createFixture(); this.createFixture();
this.body.ResetMassData(); this.body.resetMassData();
this.flipDirection = 1; this.flipDirection = 1;
if (this.body.getMass() < 1) { if (this.body.getMass() < 1) {
this.body.SetBullet(true); this.body.setBullet(true);
} }
nc.trigger(nc.ns.core.game.worldUpdateObjects.add, this); nc.trigger(nc.ns.core.game.worldUpdateObjects.add, this);
@ -124,8 +124,8 @@ function (Parent, Box2D, optionsHelper, Settings, Exception, nc, Assert) {
handPosition.x + ((this.options.width / Settings.RATIO / 2) * direction), handPosition.x + ((this.options.width / Settings.RATIO / 2) * direction),
handPosition.y handPosition.y
); );
this.body.SetPosition(position); this.body.setPosition(position);
this.body.SetAngle((this.options.grabAngle || 0.0) * direction); this.body.setAngle((this.options.grabAngle || 0.0) * direction);
this.flip(direction); this.flip(direction);
}; };
@ -148,7 +148,7 @@ function (Parent, Box2D, optionsHelper, Settings, Exception, nc, Assert) {
var x = options.x * Settings.MAX_THROW_FORCE / this.options.weight + carrierVelocity.x; var x = options.x * Settings.MAX_THROW_FORCE / this.options.weight + carrierVelocity.x;
var y = -options.y * Settings.MAX_THROW_FORCE / this.options.weight + carrierVelocity.y; var y = -options.y * Settings.MAX_THROW_FORCE / this.options.weight + carrierVelocity.y;
var vector = planck.Vec2(x, y); var vector = planck.Vec2(x, y);
body.SetLinearVelocity(vector); body.setLinearVelocity(vector);
var av = -options.av * Settings.MAX_THROW_ANGULAR_VELOCITY; var av = -options.av * Settings.MAX_THROW_ANGULAR_VELOCITY;
body.setAngularVelocity(av); body.setAngularVelocity(av);

View file

@ -8,7 +8,7 @@ define([
"Game/Config/ItemSettings", "Game/Config/ItemSettings",
], ],
function (Parent, Box2D, Settings, nc, Assert, optionsHelper, ItemSettings) { function (Parent, planck, Settings, nc, Assert, optionsHelper, ItemSettings) {
"use strict"; "use strict";

View file

@ -4,7 +4,7 @@ define([
"Game/Config/Settings" "Game/Config/Settings"
], ],
function (Parent, Box2D, Settings) { function (Parent, planck, Settings) {
"use strict"; "use strict";

View file

@ -9,7 +9,7 @@ define([
// "json!Game/Asset/RubeDoll.json" // Temporarily disabled during Planck.js migration // "json!Game/Asset/RubeDoll.json" // Temporarily disabled during Planck.js migration
], ],
function (Parent, /* RubeLoader, */ Box2D, Settings, Assert, nc, Matrix /* , RubeDollJson */) { function (Parent, /* RubeLoader, */ planck, Settings, Assert, nc, Matrix /* , RubeDollJson */) {
"use strict"; "use strict";

View file

@ -5,7 +5,7 @@ define([
"Lib/Utilities/Assert" "Lib/Utilities/Assert"
], ],
function (Parent, Box2D, Settings, Assert) { function (Parent, planck, Settings, Assert) {
"use strict"; "use strict";
@ -159,15 +159,7 @@ function (Parent, Box2D, Settings, Assert) {
var wheelBody = this.body.getWorld().createBody(bodyDef); var wheelBody = this.body.getWorld().createBody(bodyDef);
wheelBody.createFixture(fixtureDef); wheelBody.createFixture(fixtureDef);
//var revoluteJointDef = new Box2D.Dynamics.Joints.b2RevoluteJointDef(); // See top of file for Planck.js joint creation reference.
var revoluteJointDef = new Box2D.Dynamics.Joints.b2WeldJointDef();
//revoluteJointDef.enableMotor = false;
revoluteJointDef.initialize(this.body, wheelBody, wheelBody.getWorldCenter());
var j = this.body.getWorld().createJoint(revoluteJointDef);
// FIXME this means, that we will have bodies in the world, which must not be // FIXME this means, that we will have bodies in the world, which must not be
// updated (wheels) because they are always connected to a body which will be updated. // updated (wheels) because they are always connected to a body which will be updated.

View file

@ -3,7 +3,7 @@ define([
"Lib/Vendor/Planck" "Lib/Vendor/Planck"
], ],
function (Parent, Box2D) { function (Parent, planck) {
"use strict"; "use strict";

View file

@ -10,7 +10,7 @@ define([
"Game/" + GLOBALS.context + "/GameObjects/Items/RagDoll", "Game/" + GLOBALS.context + "/GameObjects/Items/RagDoll",
"Game/" + GLOBALS.context + "/GameObjects/Items/RubeDoll" "Game/" + GLOBALS.context + "/GameObjects/Items/RubeDoll"
], function (Settings, Box2D, nc, Abstract, CollisionDetector, Tile, Item, Skateboard, RagDoll, RubeDoll) { ], function (Settings, planck, nc, Abstract, CollisionDetector, Tile, Item, Skateboard, RagDoll, RubeDoll) {
"use strict"; "use strict";

View file

@ -13,7 +13,7 @@ define([
"Game/" + GLOBALS.context + "/GameObjects/Item", "Game/" + GLOBALS.context + "/GameObjects/Item",
"Game/" + GLOBALS.context + "/GameObjects/Items/Skateboard", "Game/" + GLOBALS.context + "/GameObjects/Items/Skateboard",
], function (Parent, Settings, ItemSettings, Box2D, optionsHelper, Exception, nc, Assert, AbstractLayer, CollisionDetector, Tile, Item, Skateboard) { ], function (Parent, Settings, ItemSettings, planck, optionsHelper, Exception, nc, Assert, AbstractLayer, CollisionDetector, Tile, Item, Skateboard) {
"use strict"; "use strict";

View file

@ -172,6 +172,14 @@ function (Settings, ColorConverter, Exception, pointerLockManager, qs) {
function populateMaps() { function populateMaps() {
ajax("getMaps", {}, function(responseText) { ajax("getMaps", {}, function(responseText) {
var maps = JSON.parse(responseText).success; var maps = JSON.parse(responseText).success;
// Sort maps to put debug at the end
maps.sort(function(a, b) {
if (a === "debug") return 1;
if (b === "debug") return -1;
return a.localeCompare(b);
});
var html = ""; var html = "";
for (var i = 0; i < maps.length; i++) { for (var i = 0; i < maps.length; i++) {
var map = maps[i]; var map = maps[i];