Base platformer structure
with matter.js physics engine
WASD and mouse to move
A Pen by lilgreenland on CodePen.
Base platformer structure
with matter.js physics engine
WASD and mouse to move
A Pen by lilgreenland on CodePen.
<canvas id="canvas"></canvas> | |
<svg class='background'> | |
<defs> | |
<style type="text/css"><![CDATA[ | |
.keyrect { | |
stroke: #333; | |
fill: "#555"; | |
rx: 5; | |
} | |
]]></style> | |
</defs> | |
<g id = 'background'> | |
<circle cx="500" cy="-300" r="100" fill="#d67" opacity='1' stroke-width="0"/> | |
<!-- <rect x="500" y="664" width="20" height="237" fill='#999' stroke-width="0"/> | |
<rect x="880" y="664" width="20" height="237" fill='#999' stroke-width="0"/> | |
<rect x="350" y="300" width="340" height="600" fill='#d0d0d0' stroke-width="0"/> | |
<rect x="1350" y="300" width="340" height="600" fill='#d0d0d0' stroke-width="0"/> | |
<rect x="123" y="500" width="1000" height="400" fill='#cccccc' stroke-width="0"/> --> | |
<!--<circle cx="730" cy="800" r="20" stroke="red" fill="transparent" stroke-width="4"/> --> | |
</g> | |
</svg> | |
<svg class='foreground'> | |
<defs> | |
<linearGradient id="vert_shadow" x1="0" x2="0" y1="0" y2="1"> | |
<stop offset="0%" stop-color="#000" stop-opacity="0.3"/> | |
<stop offset="100%" stop-color="#000" stop-opacity="0.1"/> | |
</linearGradient>\ | |
<linearGradient id="sunset" x1="0" x2="0" y1="0" y2="1"> | |
<stop offset="0%" stop-color="#f00" stop-opacity="0.3"/> | |
<stop offset="100%" stop-color="#000" stop-opacity="0"/> | |
</linearGradient> | |
<radialGradient id="darkcircle"> | |
<stop offset="0%" stop-color="#000" stop-opacity="0.6"/> | |
<stop offset="100%" stop-color="#000" stop-opacity="0"/> | |
</radialGradient> | |
<radialGradient id="sunsetRad"> | |
<stop offset="0%" stop-color="#f00" stop-opacity="0.4"/> | |
<stop offset="100%" stop-color="#000" stop-opacity="0"/> | |
</radialGradient> | |
<style type="text/css"><![CDATA[ | |
.keyrect { | |
stroke: #333; | |
fill: "#555"; | |
rx: 5; | |
} | |
]]></style> | |
</defs> | |
<g id = 'foreground'> | |
<!-- <rect x="-4000" y="-1000" width="13000" height="3000" fill="url(#sunset)" stroke-width="0"/> --> | |
<circle cx="500" cy="-300" r="1200" fill='url(#sunsetRad)' stroke-width="0"/> | |
<rect x="450" y="664" width="500" height="237" fill='#000' opacity="0.1" stroke-width="0"/> | |
<rect x="-600" y="800" width="400" height="100" fill='url(#vert_shadow)' stroke-width="0"/> | |
</g> | |
</svg> | |
<svg class='intro' onclick="runPlatformer(this)" viewBox="0 0 800 800"> | |
<defs> | |
<style type="text/css"><![CDATA[ | |
.keyrect { | |
stroke: #333; | |
fill: "#555"; | |
rx: 5; | |
} | |
.letter { | |
fill: #ccc; | |
font-family: sans-serif; | |
font-weight: bold; | |
font-size: 40px; | |
} | |
.instructions { | |
fill: #555; | |
font-family: sans-serif; | |
font-size: 40px; | |
} | |
.title { | |
fill: #333; | |
font-family: sans-serif; | |
font-weight: bold; | |
font-size: 100px; | |
} | |
]]></style> | |
</defs> | |
<g id = "controls"> | |
<text id = "title" class = "title" text-anchor="middle" x="400" y="200">side scroller</text> | |
<text id = "instruct" class = "instructions" text-anchor="middle" x="400" y="750">click to begin</text> | |
<g transform = "translate(130,380)"> | |
<!-- <rect class = "keyrect" id="q" x="0" y="0" width="60" height="60" fill='#99a' stroke-width="0"/> | |
<text class = "letter" text-anchor="middle" x="30" y="45">Q</text> --> | |
<rect class = "keyrect" id="e" x="140" y="0" width="60" height="60" fill='#99a' stroke-width="0"/> | |
<text class = "letter" text-anchor="middle" x="170" y="45">E</text> | |
<rect class = "keyrect" id="w" x="70" y="0" width="60" height="60" fill='#555' stroke-width="0"/> | |
<text class = "letter" text-anchor="middle" x="100" y="45">W</text> | |
<rect class = "keyrect" id="s" x="70" y="70" width="60" height="60" fill='#555' stroke-width="0"/> | |
<text class = "letter" text-anchor="middle" x="100" y="113">S</text> | |
<rect class = "keyrect" id="d" x="140" y="70" width="60" height="60" fill='#555' stroke-width="0"/> | |
<text class = "letter" text-anchor="middle" x="170" y="113">D</text> | |
<rect class = "keyrect" id="a" x="0" y="70" width="60" height="60" fill='#555' stroke-width="0"/> | |
<text class = "letter" text-anchor="middle" x="30" y="113">A</text> | |
</g> | |
<g transform = "translate(480,350) scale(0.35)"> | |
<path fill="#555" d="M284.5,182c0,15.74-12.76,28.5-28.5,28.5l0,0c-15.74,0-28.5-12.76-28.5-28.5v-49 | |
c0-15.74,12.76-28.5,28.5-28.5l0,0c15.74,0,28.5,12.76,28.5,28.5V182z"/> | |
<g> | |
<path fill="#555" d="M274,88.035c15.071,6.217,25.557,19.987,25.557,35.988v67.953c0,16.001-10.485,29.771-25.557,35.988V264.5 | |
h147V141.806C421,72.325,362.797,16,291,16h-17V88.035z"/> | |
<path fill="#555" d="M91,275v95.193C91,439.675,149.203,496,221,496h70c71.797,0,130-56.325,130-125.807V275H91z"/> | |
<path fill="#555" d="M238,264.5v-36.535c-15.071-6.217-25.557-19.987-25.557-35.988v-67.953 | |
c0-16.001,10.485-29.771,25.557-35.988V16h-17C149.203,16,91,72.325,91,141.806V264.5H238z"/> | |
</g> | |
</g> | |
</g> | |
</svg> |
Base platformer structure
with matter.js physics engine
WASD and mouse to move
A Pen by lilgreenland on CodePen.
A Pen by lilgreenland on CodePen.
//matter.js version: edge (Oct 2 2016) | |
/* TODO: ************************************************************** | |
make a graphics that looks like the player has (loose wires / a tail / a rope) to indicate player motion | |
draw images on top of bodies | |
make an svg convert to png and add it to canvas | |
add a foreground layer for shadows and lights stuff in front of player | |
add more methods of player interaction | |
portals | |
need to find a way to fire the portals at locations | |
use raycasting in matter.js | |
they could only interact with statics | |
gun | |
you'd have to add bad guys too of course... | |
game mechanics | |
mechanics that support the physics engine | |
add rope/constraint | |
store/spawn bodies in player (like starfall) | |
get ideas from game: limbo / inside | |
environmental hazards | |
laser | |
lava | |
button / switch | |
door | |
fizzler | |
moving platform | |
map zones | |
water | |
low friction ground | |
bouncy ground | |
give each foot a sensor to check for ground collisions | |
feet with not go into the ground even on slanted ground | |
this might be not worth it, but it might look really cool | |
build a method for drawing all constaints | |
but not player constaint | |
track foot positions with velocity better as the player walks/crouch/runs | |
BUGS************************************************************ | |
sensor triggers sometimes off of the box of a triangle or hexagon or nonrectangle vertex defined body | |
not occuring after adding sensor to player body composite (no idea why that worked...) | |
done | |
crouch doesn't actually trigger headsensor T/F if the player is touching stuff? | |
pause in matter isn't working/possible?? | |
slowing time down makes all the bodies bounce around. | |
holding a body with a constraint pushes on other bodies too easily | |
mostly fixed by capping the mass of what player can hold | |
*/ | |
//set up canvas | |
const canvas = document.getElementById('canvas'); | |
const ctx = canvas.getContext("2d"); | |
function setupCanvas() { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
ctx.font = "17px Arial"; | |
ctx.lineJoin = 'round'; | |
ctx.lineCap = "round"; | |
} | |
setupCanvas(); | |
window.onresize = function() { | |
setupCanvas(); | |
}; | |
//mouse move input | |
window.onmousemove = function(e) { | |
mech.getMousePos(e.clientX, e.clientY); | |
}; | |
//mouse click input | |
//keyboard input | |
var keys = []; | |
document.body.addEventListener("keyup", function(e) { | |
keys[e.keyCode] = false; | |
}); | |
document.body.addEventListener("keydown", function(e) { | |
keys[e.keyCode] = true; | |
if (keys[84]) { //t = testing mode | |
if (game.testing) { | |
game.testing = false; | |
} else { | |
game.testing = true; | |
} | |
} | |
}); | |
var stats = new Stats(); //setup stats library to show FPS | |
stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom | |
//stats.domElement.style.opacity = '0.5' | |
// game Object Prototype ********************************************* | |
//********************************************************************* | |
var gameProto = function(){ | |
this.testing = false; //testing mode: shows wireframe and some variables | |
//time related vars and methods | |
this.cycle = 0; | |
this.lastTimeStamp = 0; //tracks time stamps for measuing delta | |
this.delta = 0; //measures how slow the engine is running compared to 60fps | |
this.timing = function() { | |
this.cycle++; //tracks game cycles | |
//delta is used to adjust forces on game slow down; | |
this.delta = (engine.timing.timestamp-this.lastTimeStamp)/16.666666666666; | |
this.lastTimeStamp = engine.timing.timestamp; //track last engine timestamp | |
} | |
} | |
var game = new gameProto(); | |
// player Object Prototype ********************************************* | |
//********************************************************************* | |
var mechProto = function() { | |
this.width = 50; | |
this.radius = 30; | |
this.stroke = "#333"; | |
this.fill = "#eee"; | |
this.restHeight = 95; | |
this.yOff = 52; | |
this.yOffGoal = 52; | |
this.onGround = false; //checks if on ground or in air | |
this.crouch = false; | |
this.isHeadClear = true; | |
this.spawnPos = { | |
x: 0, | |
y: 300 | |
}; | |
this.spawnVel = { | |
x: 0, | |
y: 0 | |
}; | |
this.x = this.spawnPos.x; | |
this.y = this.spawnPos.y; | |
this.Sy = this.y; //adds a smoothing effect to vertical only | |
this.Vx = 0; | |
this.VxMax = 7; | |
this.Vy = 0; | |
this.mass = 5; | |
this.Fx = 0.004 * this.mass; //run Force on ground | |
this.FxAir = 0.0006 * this.mass; //run Force in Air | |
this.Fy = -0.04 * this.mass; //jump Force | |
this.angle = 0; | |
this.walk_cycle = 0; | |
this.stepSize = 0; | |
this.flipLegs = -1; | |
this.hip = { | |
x: 12, | |
y: 24, | |
}; | |
this.knee = { | |
x: 0, | |
y: 0, | |
x2: 0, | |
y2: 0 | |
}; | |
this.foot = { | |
x: 0, | |
y: 0 | |
}; | |
this.legLength1 = 55; | |
this.legLength2 = 45; | |
this.canvasX = canvas.width / 2; | |
this.canvasY = canvas.height / 2; | |
this.transX = this.canvasX - this.x; | |
this.transY = this.canvasX - this.x; | |
this.mouse = { | |
x: canvas.width / 3, | |
y: canvas.height | |
}; | |
this.getMousePos = function(x, y) { | |
this.mouse.x = x; | |
this.mouse.y = y; | |
}; | |
this.testingControls = function() { | |
//move | |
Matter.Body.setAngularVelocity(player, 0); //keep player from rotating | |
this.x = player.position.x; | |
this.y = playerBody.position.y - this.yOff; | |
this.Vx = player.velocity.x; | |
this.Vy = player.velocity.y; | |
//look | |
this.canvasX = canvas.width / 2 | |
this.canvasY = canvas.height / 2 | |
this.transX = this.canvasX - this.x; | |
this.transY = this.canvasY - this.y; | |
this.angle = Math.atan2(this.mouse.y - this.canvasY, this.mouse.x - this.canvasX); | |
} | |
this.move = function() { | |
Matter.Body.setAngularVelocity(player, 0); //keep player from rotating | |
this.x = player.position.x; | |
//this.y = player.position.y - (this.height + 30) / 2 + 30; | |
//looking at player body, to ignore the other parts of the player composite | |
this.y = playerBody.position.y - this.yOff; | |
this.Vx = player.velocity.x; | |
this.Vy = player.velocity.y; | |
}; | |
this.look = function() { | |
//this.angle = Math.atan2(this.mouse.y - this.y, this.mouse.x - this.x); | |
//set a max on mouse look | |
var mX = this.mouse.x; | |
if (mX > canvas.width * 0.8) { | |
mX = canvas.width * 0.8; | |
} else if (mX < canvas.width * 0.2) { | |
mX = canvas.width * 0.2; | |
} | |
var mY = this.mouse.y; | |
if (mY > canvas.height * 0.8) { | |
mY = canvas.height * 0.8; | |
} else if (mY < canvas.height * 0.2) { | |
mY = canvas.height * 0.2; | |
} | |
//set mouse look | |
this.canvasX = this.canvasX * 0.94 + (canvas.width - mX) * 0.06; | |
this.canvasY = this.canvasY * 0.94 + (canvas.height - mY) * 0.06; | |
//set translate values | |
this.transX = this.canvasX - this.x; | |
this.Sy = 0.98 * this.Sy + 0.02 * (this.y); | |
this.transY = this.canvasY - this.Sy; | |
//rapid smoothing if player goes off window | |
// if (this.transY > -300 || this.transY < -canvas.height+300){ | |
// this.Sy = 0.96 * this.Sy + 0.04 * (this.y); | |
// this.transY = this.canvasY - this.Sy; | |
// } | |
//make player head angled towards mouse | |
this.angle = Math.atan2(this.mouse.y - this.canvasY, this.mouse.x - this.canvasX); | |
}; | |
this.buttonCD_jump = 0; //cooldown for player buttons | |
this.keyMove = function() { | |
if (this.onGround) { //on ground ********************** | |
if (keys[40] || keys[83]) { //on ground && pressing crouch:down/ s | |
if (!this.crouch) { //on first crouch cycle shorten the player body | |
this.crouch = true; | |
headSensor.isSensor = true; | |
} | |
} else if (this.crouch) { //on ground && not pressing crouch && crouch is true | |
if (this.isHeadClear) { | |
this.crouch = false; | |
headSensor.isSensor = false; | |
} | |
//on ground && !pressing crouch && !crouching && pressing jump: up/w/space && it is >20 cycles from last jump | |
} else if ((keys[32] || keys[38] || keys[87]) && this.buttonCD_jump + 20 < game.cycle) { | |
this.buttonCD_jump = game.cycle; //can't jump until 20 cycles pass | |
Matter.Body.setVelocity(player, { //zero player velocity for consistant jumps | |
x: player.velocity.x, | |
y: 0 | |
}); | |
player.force.y = this.Fy/game.delta; //jump force / delta so that force is the same on game slowdowns | |
} | |
//set correct height and friction | |
if (this.crouch) { | |
player.frictionAir = 0.5; | |
this.yOffGoal = 25; | |
} else if (this.onGround) { | |
player.frictionAir = 0.12; | |
this.yOffGoal = 52; | |
} | |
//horizontal move on ground | |
if (keys[37] || keys[65]) { //left or a | |
if (player.velocity.x > -this.VxMax) { | |
player.force.x = -this.Fx/game.delta; | |
} | |
} else if (keys[39] || keys[68]) { //right or d | |
if (player.velocity.x < this.VxMax) { | |
player.force.x = this.Fx/game.delta; | |
} | |
} | |
} else { // in air ********************************** | |
if (!this.crouch){ | |
this.yOffGoal = 70; | |
} | |
player.frictionAir = 0.001; | |
//check for short jumps | |
if (this.buttonCD_jump + 60 > game.cycle && //just pressed jump | |
!(keys[32] || keys[38] || keys[87]) && //but not pressing jump key | |
this.Vy < 0) { // and velocity is up | |
Matter.Body.setVelocity(player, { //reduce player velocity every cycle until not true | |
x: player.velocity.x, | |
y: player.velocity.y * 0.94 | |
}); | |
} | |
if (keys[37] || keys[65]) { // move player left / a | |
if (player.velocity.x > -this.VxMax + 2) { | |
player.force.x = -this.FxAir/game.delta; | |
} | |
} else if (keys[39] || keys[68]) { //move player right / d | |
if (player.velocity.x < this.VxMax - 2) { | |
player.force.x = this.FxAir/game.delta; | |
} | |
} | |
} | |
//smoothly move height towards height goal ************ | |
this.yOff = this.yOff * 0.85 + this.yOffGoal * 0.15 | |
}; | |
this.deathCheck = function() { | |
if (this.y > 5000) { // if player is 5000px deep reset to spawn Position/Velocity | |
Matter.Body.setPosition(player, this.spawnPos); | |
Matter.Body.setVelocity(player, this.spawnVel); | |
this.dropBody(); | |
} | |
}; | |
this.holdKeyDown = 0; | |
this.buttonCD_hold = 0; //cooldown for player buttons | |
this.keyHold = function() { //checks for holding/dropping/picking up bodies | |
if (this.isHolding) { | |
//give the constaint more length and less stiffness if it is pulled out of position | |
var Dx = body[this.holdingBody].position.x - holdConstraint.pointA.x; | |
var Dy = body[this.holdingBody].position.y - holdConstraint.pointA.y; | |
holdConstraint.length = Math.sqrt(Dx * Dx + Dy * Dy) * 0.95; | |
holdConstraint.stiffness = -0.01 * holdConstraint.length + 1; | |
if (holdConstraint.length > 100) this.dropBody(); //drop it if the constraint gets too long | |
holdConstraint.pointA = { //set constraint position | |
x: this.x + 50 * Math.cos(this.angle), //just in front of player nose | |
y: this.y + 50 * Math.sin(this.angle) | |
}; | |
if (keys[81]) { // q = rotate the body | |
body[this.holdingBody].torque = 0.05*body[this.holdingBody].mass; | |
} | |
//look for dropping held body | |
if (this.buttonCD_hold < game.cycle) { | |
if (keys[69]) { //if holding e drops | |
this.holdKeyDown++; | |
} else if (this.holdKeyDown && !keys[69]) { | |
this.dropBody(); //if you hold down e long enough the body is thrown | |
this.throwBody(); | |
} | |
} | |
} else if (keys[69]) { //when not holding e = pick up body | |
this.findClosestBody(); | |
if (this.closest.dist2 < 10000) { //pick up if distance closer then 100*100 | |
this.isHolding = true; | |
this.holdKeyDown = 0; | |
this.buttonCD_hold = game.cycle + 20; | |
this.holdingBody = this.closest.index; | |
//body[this.closest.index].isSensor = true; //sensor seems a bit inconsistant | |
body[this.holdingBody].collisionFilter.group = -2; //don't collide with player | |
body[this.holdingBody].frictionAir = 0.1; //makes the holding body less jittery | |
holdConstraint.bodyB = body[this.holdingBody]; | |
holdConstraint.length = 0; | |
holdConstraint.pointA = { | |
x: this.x + 50 * Math.cos(this.angle), | |
y: this.y + 50 * Math.sin(this.angle) | |
}; | |
} | |
} | |
}; | |
this.dropBody = function() { | |
var timer; //reset player collision | |
function resetPlayerCollision() { | |
timer = setTimeout(function(){ | |
var dx = mech.x - body[mech.holdingBody].position.x | |
var dy = mech.y - body[mech.holdingBody].position.y | |
if (dx*dx+dy*dy > 20000){ | |
body[mech.holdingBody].collisionFilter.group = 2; //can collide with player | |
} else{ | |
resetPlayerCollision(); | |
} | |
}, 100); | |
} | |
resetPlayerCollision(); | |
this.isHolding = false; | |
body[this.holdingBody].frictionAir = 0.01; | |
holdConstraint.bodyB = jumpSensor; //set on sensor to get the constaint on somethign else | |
}; | |
this.throwMax = 150; | |
this.throwBody = function() { | |
var throwMag = 0; | |
if (this.holdKeyDown > 20) { | |
if (this.holdKeyDown > this.throwMax) this.holdKeyDown = this.throwMax; | |
//scale fire with mass and with holdKeyDown time | |
throwMag = body[this.holdingBody].mass * this.holdKeyDown * 0.001; | |
} | |
body[this.holdingBody].force.x = throwMag * Math.cos(this.angle); | |
body[this.holdingBody].force.y = throwMag * Math.sin(this.angle); | |
}; | |
this.isHolding = false; | |
this.holdingBody = 0; | |
this.closest = { | |
dist2: 1000000, | |
index: 0 | |
}; | |
this.findClosestBody = function() { | |
this.closest.dist2 = 100000; | |
for (var i = 0; i < body.length; i++) { | |
var Px = body[i].position.x - (this.x + 50 * Math.cos(this.angle)); | |
var Py = body[i].position.y - (this.y + 50 * Math.sin(this.angle)); | |
if (body[i].mass < player.mass && Px * Px + Py * Py < this.closest.dist2) { | |
this.closest.dist2 = Px * Px + Py * Py; | |
this.closest.index = i; | |
} | |
} | |
}; | |
/* this.forcePoke = function() { | |
for (var i = 0; i < body.length; i++) { | |
var Dx = body[i].position.x - (this.mouse.x - this.transX); | |
var Dy = body[i].position.y - (this.mouse.y - this.transY); | |
var accel = 0.2 / Math.sqrt(Dx * Dx + Dy * Dy); | |
if (accel > 0.01) accel = 0.01; //cap accel | |
accel = accel * body[i].mass //scale with mass | |
var angle = Math.atan2(Dy, Dx); | |
body[i].force.x -= accel * Math.cos(angle); | |
body[i].force.y -= accel * Math.sin(angle); | |
} | |
}; */ | |
this.drawLeg = function(stroke) { | |
ctx.save(); | |
ctx.translate(this.x, this.y); | |
ctx.scale(this.flipLegs, 1); | |
//leg lines | |
ctx.strokeStyle = stroke; | |
ctx.lineWidth = 7; | |
ctx.beginPath(); | |
ctx.moveTo(this.hip.x, this.hip.y); | |
ctx.lineTo(this.knee.x, this.knee.y); | |
ctx.lineTo(this.foot.x, this.foot.y); | |
ctx.stroke(); | |
//toe lines | |
ctx.lineWidth = 4; | |
ctx.beginPath(); | |
ctx.moveTo(this.foot.x, this.foot.y); | |
ctx.lineTo(this.foot.x - 15, this.foot.y + 5); | |
ctx.moveTo(this.foot.x, this.foot.y); | |
ctx.lineTo(this.foot.x + 15, this.foot.y + 5); | |
ctx.stroke(); | |
//hip joint | |
ctx.strokeStyle = this.stroke; | |
ctx.fillStyle = this.fill; | |
ctx.lineWidth = 2; | |
ctx.beginPath(); | |
ctx.arc(this.hip.x, this.hip.y, 11, 0, 2 * Math.PI); | |
ctx.fill(); | |
ctx.stroke(); | |
//knee joint | |
ctx.beginPath(); | |
ctx.arc(this.knee.x, this.knee.y, 7, 0, 2 * Math.PI); | |
ctx.fill(); | |
ctx.stroke(); | |
//foot joint | |
ctx.beginPath(); | |
ctx.arc(this.foot.x, this.foot.y, 6, 0, 2 * Math.PI); | |
ctx.fill(); | |
ctx.stroke(); | |
ctx.restore(); | |
}; | |
this.calcLeg = function(cycle_offset, offset) { | |
this.hip.x = 12 + offset; | |
this.hip.y = 24 + offset; | |
//stepSize goes to zero if Vx is zero or not on ground (make this transition cleaner) | |
//changes to stepsize are smoothed by adding only a percent of the new value each cycle | |
this.stepSize = 0.9 * this.stepSize + 0.1 * (8 * Math.sqrt(Math.abs(this.Vx)) * this.onGround); | |
var stepAngle = 0.037 * this.walk_cycle + cycle_offset; | |
this.foot.x = 2 * this.stepSize * Math.cos(stepAngle) + offset; | |
this.foot.y = offset + this.stepSize * Math.sin(stepAngle) + this.yOff + 42; | |
var Ymax = this.yOff + 42; //95 | |
if (this.foot.y > Ymax) this.foot.y = Ymax; | |
//calculate knee position as intersection of circle from hip and foot | |
var d = Math.sqrt((this.hip.x - this.foot.x) * (this.hip.x - this.foot.x) + | |
(this.hip.y - this.foot.y) * (this.hip.y - this.foot.y)); | |
var l = (this.legLength1 * this.legLength1 - this.legLength2 * this.legLength2 + d * d) / (2 * d); | |
var h = Math.sqrt(this.legLength1 * this.legLength1 - l * l); | |
this.knee.x = l / d * (this.foot.x - this.hip.x) - h / d * (this.foot.y - this.hip.y) + this.hip.x + offset; | |
this.knee.y = l / d * (this.foot.y - this.hip.y) + h / d * (this.foot.x - this.hip.x) + this.hip.y; | |
}; | |
this.draw = function() { | |
ctx.fillStyle = this.fill; | |
if (this.mouse.x > canvas.width / 2) { | |
this.flipLegs = 1; | |
} else { | |
this.flipLegs = -1; | |
} | |
this.walk_cycle += this.flipLegs * this.Vx; | |
this.calcLeg(Math.PI, -3); | |
this.drawLeg('#444'); | |
this.calcLeg(0, 0); | |
this.drawLeg('#333'); | |
//draw body | |
ctx.save(); | |
ctx.translate(this.x, this.y); | |
ctx.rotate(this.angle); | |
ctx.strokeStyle = this.stroke; | |
ctx.lineWidth = 2; | |
//ctx.fillStyle = this.fill; | |
var grd = ctx.createLinearGradient(-30, 0, 30, 0); | |
grd.addColorStop(0, "#bbb"); | |
grd.addColorStop(1, "#fff"); | |
ctx.fillStyle = grd; | |
ctx.beginPath(); | |
//ctx.moveTo(0, 0); | |
ctx.arc(0, 0, 30, 0, 2 * Math.PI); | |
ctx.arc(15, 0, 4, 0, 2 * Math.PI); | |
ctx.fill(); | |
ctx.stroke(); | |
ctx.restore(); | |
//draw holding graphics | |
if (this.isHolding) { | |
if (this.holdKeyDown > 20) { | |
if (this.holdKeyDown > this.throwMax) { | |
ctx.strokeStyle = 'rgba(255, 0, 255, 0.8)'; | |
} else { | |
ctx.strokeStyle = 'rgba(255, 0, 255, ' + (0.2 + 0.4 * this.holdKeyDown / this.throwMax) + ')'; | |
} | |
} else { | |
ctx.strokeStyle = 'rgba(0, 255, 255, 0.2)'; | |
} | |
ctx.lineWidth = 10; | |
ctx.beginPath(); | |
ctx.moveTo(holdConstraint.bodyB.position.x + Math.random() * 2, | |
holdConstraint.bodyB.position.y + Math.random() * 2); | |
ctx.lineTo(this.x + 15 * Math.cos(this.angle), this.y + 15 * Math.sin(this.angle)); | |
//ctx.lineTo(holdConstraint.pointA.x,holdConstraint.pointA.y); | |
ctx.stroke(); | |
} | |
}; | |
this.info = function() { | |
var line = 80; | |
ctx.fillStyle = "#000"; | |
ctx.fillText("Press T to exit testing mode", 5, line); | |
line += 30; | |
ctx.fillText("cycle = " + game.cycle, 5, line); | |
line += 20; | |
ctx.fillText("delta = " + game.delta.toFixed(6), 5, line); | |
line += 20; | |
ctx.fillText("mX = " + (this.mouse.x - this.transX).toFixed(2), 5, line); | |
line += 20; | |
ctx.fillText("mY = " + (this.mouse.y - this.transY).toFixed(2), 5, line); | |
line += 20; | |
ctx.fillText("x = " + this.x.toFixed(0), 5, line); | |
line += 20; | |
ctx.fillText("y = " + this.y.toFixed(0), 5, line); | |
line += 20; | |
ctx.fillText("Vx = " + this.Vx.toFixed(2), 5, line); | |
line += 20; | |
ctx.fillText("Vy = " + this.Vy.toFixed(2), 5, line); | |
line += 20; | |
ctx.fillText("Fx = " + player.force.x.toFixed(3), 5, line); | |
line += 20; | |
ctx.fillText("Fy = " + player.force.y.toFixed(3), 5, line); | |
line += 20; | |
ctx.fillText("yOff = " + this.yOff.toFixed(1), 5, line); | |
line += 20; | |
ctx.fillText("mass = " + player.mass.toFixed(1), 5, line); | |
line += 20; | |
ctx.fillText("onGround = " + this.onGround, 5, line); | |
line += 20; | |
ctx.fillText("crouch = " + this.crouch, 5, line); | |
line += 20; | |
ctx.fillText("isHeadClear = " + this.isHeadClear, 5, line); | |
line += 20; | |
ctx.fillText("HeadIsSensor = " + headSensor.isSensor, 5, line); | |
line += 20; | |
ctx.fillText("frictionAir = " + player.frictionAir.toFixed(3), 5, line); | |
line += 20; | |
ctx.fillText("stepSize = " + this.stepSize.toFixed(2), 5, line); | |
}; | |
}; | |
var mech = new mechProto(); | |
//add in mouse click events | |
// window.onclick = function(e) { | |
// }; | |
//matter.js *********************************************************** | |
//********************************************************************* | |
//********************************************************************* | |
// module aliases | |
var Engine = Matter.Engine, | |
World = Matter.World, | |
Events = Matter.Events, | |
Composites = Matter.Composites, | |
Composite = Matter.Composite, | |
Constraint = Matter.Constraint, | |
Vertices = Matter.Vertices, | |
Query = Matter.Query, | |
Body = Matter.Body, | |
Bodies = Matter.Bodies; | |
// create an engine | |
var engine = Engine.create(); | |
//define player ************************************************************* | |
//*************************************************************************** | |
// var playerBody = Bodies.rectangle(0, 0, 40, 130); | |
//player as a series of vertices | |
//var playerVector = Vertices.fromPath('0 0 0 130 50 130 50 0'); | |
//var playerVector = Vertices.fromPath('0 0 0 115 20 130 30 130 50 115 50 0'); | |
var playerVector = Vertices.fromPath('0 40 0 115 20 130 30 130 50 115 50 40'); | |
var playerBody = Matter.Bodies.fromVertices(0, 0, playerVector); | |
var headSensor = Bodies.rectangle(0, -62, 50, 40, { | |
isSensor: false, | |
collisionFilter: { | |
group: -1 | |
}, | |
}); | |
//this sensor check if the player is on the ground to enable jumping | |
var jumpSensor = Bodies.rectangle(0, 46, 40, 15, { | |
isSensor: true, | |
collisionFilter: { | |
group: -1 | |
}, | |
}); | |
var player = Body.create({ //combine jumpSensor and playerBody | |
parts: [playerBody, jumpSensor, headSensor], | |
friction: 0, | |
frictionStatic: 0, | |
restitution: 0.3, | |
collisionFilter: { | |
group: -2 | |
}, | |
}); | |
Matter.Body.setPosition(player, mech.spawnPos); | |
Matter.Body.setVelocity(player, mech.spawnVel); | |
Matter.Body.setMass(player, mech.mass); | |
World.add(engine.world, [player]); | |
//holding body constraint | |
var holdConstraint = Constraint.create({ | |
pointA: { | |
x: 0, | |
y: 0 | |
}, | |
//setting constaint to jump sensor because it has to be on something until the player picks up things | |
bodyB: jumpSensor, | |
stiffness: 0.4, | |
}); | |
World.add(engine.world, holdConstraint); | |
//array that holds all the elements that are drawn by the renderer___ | |
var body = []; //non static bodies | |
var map = []; //all static bodies | |
//spawn bodies ************************************************************* | |
//*************************************************************************** | |
spawn(); | |
function spawn() { | |
function BodyRect(x, y, width, height) { //addes reactangles to map array | |
body[body.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height); | |
} | |
BodyRect(1475, 0, 100, 800); | |
var particleOptions = { | |
friction: 0.05, | |
frictionStatic: 0.1, | |
render: { | |
visible: true | |
} | |
}; | |
//body[body.length] = Composites.softBody(350, 500, 5, 5, 0, 0, true, 18,particleOptions); | |
//World.add(engine.world, Composites.softBody(300, 300, 3, 3, 0, 0, true, 18,particleOptions)); | |
/* for (var i = 0; i < 10; i++) { //random bouncy circles | |
body[body.length] = Bodies.circle(-800 + (0.5 - Math.random()) * 200, 600 + (0.5 - Math.random()) * 200, 7 + Math.ceil(Math.random() * 30), { | |
restitution: 0.8, | |
}) | |
} */ | |
for (var i = 0; i < 10; i++) { //stack of medium hexagons | |
body[body.length] = Bodies.polygon(-400, 30 - i * 70, 6, 40, { | |
angle: Math.PI / 2, | |
}); | |
} | |
for (var i = 0; i < 5; i++) { //stairs of boxes taller on left | |
for (var j = 0; j < 5 - i; j++) { | |
var r = 40; | |
body[body.length] = Bodies.rectangle(50 + r / 2 + i * r, 900 - r / 2 - i * r, r, r, { | |
restitution: 0.8, | |
}); | |
} | |
} | |
for (var i = 0; i < 10; i++) { //stairs of boxes taller on right | |
for (var j = 0; j < i; j++) { | |
var r = 120; | |
body[body.length] = Bodies.rectangle(2639 + r / 2 + i * r, 900 + r - i * r, r, r, { | |
restitution: 0.6, | |
friction: 0.3, | |
frictionStatic: 0.9, | |
}); | |
} | |
} | |
for (var i = 0; i < 12; i++) { //a stack of boxes | |
body[body.length] = Bodies.rectangle(936, 700 + i * 21, 25, 21); | |
} | |
for (var i = 0; i < 12; i++) { //a stack of boxes | |
body[body.length] = Bodies.rectangle(464, 700 + i * 21, 25, 21); | |
} | |
// body[body.length] = Bodies.circle(350, 780, 20,{ | |
// friction: 0, | |
// frictionAir: 0, | |
// frictionStatic: 0, | |
// restitution: 0, | |
// }) //medium circle | |
// constraint1 = Constraint.create({ | |
// pointA: { | |
// x: 350, | |
// y: 780 | |
// }, | |
// bodyB: body[body.length - 1], | |
// stiffness: 0.1, | |
// }) | |
// World.add(engine.world, constraint1); | |
body[body.length] = Bodies.rectangle(700, 550, 200, 10); //long box | |
//map statics ************************************************************** | |
//*************************************************************************** | |
function mapRect(x, y, width, height) { //addes reactangles to map array | |
map[map.length] = Bodies.rectangle(x + width / 2, y + height / 2, width, height); | |
} | |
function mapVertex(x, y, vector) { //addes reactangles to map array | |
map[map.length] = Matter.Bodies.fromVertices(x, y, Vertices.fromPath(vector)); | |
} | |
//mapVertex(-1700, 700, '0 0 0 -500 500 -500 1000 -400 1500 0'); //large ramp | |
mapVertex(1230, 867, '200 0 200 100 0 100'); // ramp | |
mapVertex(-1300, 700, '0 0 -500 0 -500 200'); //angeled ceiling | |
mapVertex(-1650, 700, '0 0 500 0 500 200'); //angeled ceiling | |
mapRect(650, 890, 50, 10) //ground bump | |
mapRect(-600, 0, 400, 800); //left cave | |
mapRect(-50, 700, 100, 200); //left wall | |
mapRect(0, 100, 300, 25); //left high platform | |
mapRect(550, 450, 300, 25); //wide platform | |
mapRect(650, 250, 100, 25); //wide platform | |
mapRect(1000, 450, 400, 25); //platform | |
mapRect(1200, 250, 200, 25); //platform | |
mapRect(1300, 50, 100, 25); //platform | |
//mapRect(1150, 888, 45, 20); //ground bump | |
map[map.length] = Bodies.rectangle(700, 650, 500, 30); //platform 1 | |
map[map.length] = Bodies.rectangle(1500, 850, 300, 100); //ground bump | |
map[map.length] = Bodies.rectangle(0, 1000, 4000, 200); //ground | |
map[map.length] = Bodies.rectangle(4600, 1000, 4000, 200); //far right ground | |
//add arrays to the world | |
for (var i = 0; i < body.length; i++) { | |
body[i].collisionFilter.group = 1; | |
World.add(engine.world, body[i]); //add to world | |
} | |
for (var i = 0; i < map.length; i++) { | |
map[i].collisionFilter.group = -1; | |
Matter.Body.setStatic(map[i], true); //make static | |
World.add(engine.world, map[i]); //add to world | |
} | |
} | |
// matter events ********************************************************* | |
//************************************************************************ | |
function playerGroundCheck(event, ground) { //runs on collisions events | |
var pairs = event.pairs; | |
for (var i = 0, j = pairs.length; i != j; ++i) { | |
var pair = pairs[i]; | |
if (pair.bodyA === jumpSensor) { | |
mech.onGround = ground; | |
} else if (pair.bodyB === jumpSensor) { | |
mech.onGround = ground; | |
} | |
} | |
} | |
function playerHeadCheck(event, ground) { //runs on collisions events | |
if (mech.crouch) { | |
var pairs = event.pairs; | |
for (var i = 0, j = pairs.length; i != j; ++i) { | |
var pair = pairs[i]; | |
if (pair.bodyA === headSensor) { | |
mech.isHeadClear = ground; | |
} else if (pair.bodyB === headSensor) { | |
mech.isHeadClear = ground; | |
} | |
} | |
} | |
} | |
// function holdingCheck(event, ground) { //runs on collisions events | |
// var pairs = event.pairs | |
// for (var i = 0, j = pairs.length; i != j; ++i) { | |
// var pair = pairs[i]; | |
// if (pair.bodyA === body[mech.holdingBody]) { | |
// mech.dropBody(); | |
// } else if (pair.bodyB === body[mech.holdingBody]) { | |
// mech.dropBody(); | |
// } | |
// } | |
// } | |
//determine if player is on the ground | |
Events.on(engine, "collisionStart", function(event) { | |
playerGroundCheck(event, true); | |
playerHeadCheck(event, false); | |
//if (mech.isHolding) holdingCheck(event, true) | |
}); | |
Events.on(engine, "collisionActive", function(event) { | |
playerGroundCheck(event, true); | |
playerHeadCheck(event, false); | |
}); | |
Events.on(engine, 'collisionEnd', function(event) { | |
playerGroundCheck(event, false); | |
playerHeadCheck(event, true); | |
}); | |
//runs the game loop event tick. | |
Events.on(engine, "afterTick", function(event) { | |
//cycle(); | |
}); | |
// render *********************************************************** | |
//******************************************************************* | |
function drawMatterWireFrames() { | |
var bodies = Composite.allBodies(engine.world); | |
ctx.beginPath(); | |
for (var i = 0; i < bodies.length; i += 1) { | |
var vertices = bodies[i].vertices; | |
//ctx.moveTo(bodies[i].position.x, bodies[i].position.y); | |
ctx.moveTo(vertices[0].x, vertices[0].y); | |
for (var j = 1; j < vertices.length; j += 1) { | |
ctx.lineTo(vertices[j].x, vertices[j].y); | |
} | |
ctx.lineTo(vertices[0].x, vertices[0].y); | |
} | |
ctx.lineWidth = 1; | |
ctx.strokeStyle = '#000'; | |
ctx.stroke(); | |
} | |
function drawMap() { | |
//draw map | |
ctx.beginPath(); | |
for (var i = 0; i < map.length; i += 1) { | |
var vertices = map[i].vertices; | |
ctx.moveTo(vertices[0].x, vertices[0].y); | |
for (var j = 1; j < vertices.length; j += 1) { | |
ctx.lineTo(vertices[j].x, vertices[j].y); | |
} | |
ctx.lineTo(vertices[0].x, vertices[0].y); | |
} | |
ctx.fillStyle = '#444'; | |
ctx.fill(); | |
} | |
function drawBody() { | |
//draw body | |
ctx.beginPath(); | |
for (var i = 0; i < body.length; i += 1) { | |
var vertices = body[i].vertices; | |
ctx.moveTo(vertices[0].x, vertices[0].y); | |
for (var j = 1; j < vertices.length; j += 1) { | |
ctx.lineTo(vertices[j].x, vertices[j].y); | |
} | |
ctx.lineTo(vertices[0].x, vertices[0].y); | |
} | |
ctx.lineWidth = 1.5; | |
ctx.fillStyle = '#777'; | |
ctx.fill(); | |
ctx.strokeStyle = '#222'; | |
ctx.stroke(); | |
} | |
function drawPlayerBodyTesting() { | |
//draw one body | |
ctx.beginPath(); | |
var bodyDraw = jumpSensor.vertices; | |
ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); | |
for (var j = 1; j < bodyDraw.length; j += 1) { | |
ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); | |
} | |
ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); | |
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; | |
ctx.fill(); | |
ctx.strokeStyle = '#000'; | |
ctx.stroke(); | |
//draw one body | |
ctx.beginPath(); | |
bodyDraw = playerBody.vertices; | |
ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); | |
for (var j = 1; j < bodyDraw.length; j += 1) { | |
ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); | |
} | |
ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); | |
ctx.fillStyle = 'rgba(0, 255, 0, 0.06)'; | |
ctx.fill(); | |
ctx.stroke(); | |
//draw one body | |
ctx.beginPath(); | |
bodyDraw = headSensor.vertices; | |
ctx.moveTo(bodyDraw[0].x, bodyDraw[0].y); | |
for (var j = 1; j < bodyDraw.length; j += 1) { | |
ctx.lineTo(bodyDraw[j].x, bodyDraw[j].y); | |
} | |
ctx.lineTo(bodyDraw[0].x, bodyDraw[0].y); | |
ctx.fillStyle = 'rgba(0, 0, 255, 0.3)'; | |
ctx.fill(); | |
ctx.stroke(); | |
} | |
//main loop ************************************************************ | |
//********************************************************************** | |
function cycle() { | |
stats.begin(); | |
game.timing(); | |
ctx.clearRect(0, 0, canvas.width, canvas.height); | |
mech.keyMove(); | |
mech.keyHold(); | |
if (game.testing) { | |
mech.testingControls(); | |
mech.deathCheck(); | |
ctx.save(); | |
ctx.translate(mech.transX, mech.transY); | |
mech.draw(); | |
drawMatterWireFrames(); | |
drawPlayerBodyTesting(); | |
ctx.restore(); | |
mech.info(); | |
} else { | |
mech.move(); | |
mech.deathCheck(); | |
mech.look(); | |
ctx.save(); | |
ctx.translate(mech.transX, mech.transY); | |
drawBody(); | |
mech.draw(); | |
drawMap(); | |
ctx.restore(); | |
} | |
//ctx.drawImage(space_img,1800,0); | |
//ctx.drawImage(bmo_img,-300,200); | |
//ctx.drawImage(bgtest_img,-300,200); | |
//svg graphics | |
document.getElementById('background').setAttribute( | |
'transform', 'translate(' + (mech.transX) + ',' + (mech.transY) + ')'); | |
document.getElementById('foreground').setAttribute( | |
'transform', 'translate(' + (mech.transX) + ',' + (mech.transY) + ')'); | |
stats.end(); | |
requestAnimationFrame(cycle); | |
} | |
// var bmo_img = new Image(); // Create new img element | |
// bmo_img.src = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/464612/Bmo.png'; // Set source path | |
// var bgtest_img = new Image(); // Create new img element | |
// bgtest_img.src = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/464612/backgroundtest.png'; // Set source path | |
// var space_img = new Image(); // Create new img element | |
// space_img.src = 'https://s3-us-west-2.amazonaws.com/s.cdpn.io/464612/Space.jpg'; // Set source path | |
function runPlatformer(el) { | |
el.onclick = null; //removes the onclick effect so the function only runs once | |
el.style.display = 'none'; //hides the element that spawned the function | |
document.body.appendChild( stats.dom ); //show stats.js FPS tracker | |
Engine.run(engine); //starts game engine | |
console.clear(); //gets rid of annoying console message about vertecies not working | |
requestAnimationFrame(cycle); //starts game loop | |
} |
<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/464612/matter.min.js"></script> | |
<script src="//rawgit.com/mrdoob/stats.js/master/build/stats.min.js"></script> |
sidescroller with matter.js physics engine
WASD and mouse to move
E to pick up things T for testing mode
A Pen by lilgreenland on CodePen.
sidescroller with matter.js physics engine
WASD and mouse to move
E to pick up things T for testing mode
A Pen by lilgreenland on CodePen.
body { | |
margin: 0; | |
background-color: #ddd; | |
cursor: crosshair; | |
} | |
canvas { | |
position: absolute; | |
top: 0; | |
left: 0; | |
z-index: 0; | |
} | |
svg { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
} | |
.background { | |
z-index: -1; | |
} | |
.foreground { | |
z-index: 1; | |
} | |
/* svg classes */ | |
@keyframes slideinRL { | |
0% { | |
transform: translate(-900px, 0); | |
} | |
100% { | |
transform: translate(0px, 0); | |
} | |
} | |
@keyframes slideinLR { | |
0% { | |
transform: translate(900px, 0); | |
} | |
100% { | |
transform: translate(0px, 0); | |
} | |
} | |
@keyframes fadein { | |
0% { | |
opacity: 0 | |
} | |
100% { | |
opacity: 1; | |
} | |
} | |
.intro { | |
background-color: #ddd; | |
z-index: 2; | |
} | |
#title { | |
animation: slideinRL 1s ease-out 1; | |
} | |
#instruct { | |
transform: translate(900px, 0); | |
animation: slideinLR 1s ease-out 1; | |
animation-delay: 1s; | |
animation-fill-mode: forwards; | |
} | |
/* #controls { | |
animation: fadein 3s ease-out 1; | |
} */ |