Exploring canvas by making Mega Man. I've had a life long dream to make a multiplayer Mega Man game. Maybe this is my chance!
A Pen by Drew Conley on CodePen.
| <canvas id="js-canvas" width="400" height="300"></canvas> | |
| <p>Use Arrow Keys!</p> |
Exploring canvas by making Mega Man. I've had a life long dream to make a multiplayer Mega Man game. Maybe this is my chance!
A Pen by Drew Conley on CodePen.
| const initialState = { | |
| counter: 0, | |
| canvasWidth: 400, | |
| canvasHeight: 300, | |
| characterX: 80, | |
| characterY: 0, | |
| characterWidth: 20, //hitboxW | |
| characterHeight: 24, //hitboxY | |
| inAir: true, | |
| characterFrame: 0, | |
| characterPose: [ [0,0] ], | |
| isAbleToJump: false, | |
| isFacingLeft: true, //default is Right | |
| //Jumping | |
| verticalBoost: 0, | |
| //Keyboard | |
| isKeyboardLeftPressed: false, | |
| isKeyboardRightPressed: false, | |
| walls: [ | |
| { | |
| "_id": "placement_1471799473495", | |
| "x": 0, | |
| "y": 0, | |
| "width": 16, | |
| "height": 288 | |
| }, | |
| { | |
| "_id": "placement_1471799503050", | |
| "x": 16, | |
| "y": 256, | |
| "width": 32, | |
| "height": 32 | |
| }, | |
| { | |
| "_id": "placement_1471799516043", | |
| "x": 16, | |
| "y": 128, | |
| "width": 16, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471799517145", | |
| "x": 368, | |
| "y": 144, | |
| "width": 16, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471804635390", | |
| "x": 384, | |
| "y": 0, | |
| "width": 16, | |
| "height": 288 | |
| }, | |
| { | |
| "_id": "placement_1471804662424", | |
| "x": 352, | |
| "y": 256, | |
| "width": 32, | |
| "height": 32 | |
| }, | |
| { | |
| "_id": "placement_1471804690729", | |
| "x": 256, | |
| "y": 224, | |
| "width": 80, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471804739758", | |
| "x": 144, | |
| "y": 160, | |
| "width": 32, | |
| "height": 32 | |
| }, | |
| { | |
| "_id": "placement_1471808010628", | |
| "x": 48, | |
| "y": 272, | |
| "width": 160, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471808096387", | |
| "x": 64, | |
| "y": 64, | |
| "width": 64, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471808097749", | |
| "x": 272, | |
| "y": 96, | |
| "width": 64, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471808105275", | |
| "x": 144, | |
| "y": 32, | |
| "width": 112, | |
| "height": 16 | |
| }, | |
| { | |
| "_id": "placement_1471808244933", | |
| "x": 224, | |
| "y": 128, | |
| "width": 32, | |
| "height": 32 | |
| }, | |
| { | |
| "_id": "placement_1471808330975", | |
| "x": 64, | |
| "y": 192, | |
| "width": 32, | |
| "height": 32 | |
| } | |
| ] | |
| } | |
| /* SFX */ | |
| window.landingSfx = new Howl({ | |
| urls: ['https://s3-us-west-2.amazonaws.com/s.cdpn.io/21542/06_-_MegamanLand.wav'], | |
| volume: 0.5 | |
| }); | |
| const Data = { | |
| state: {}, | |
| prevState: {}, | |
| init(initialState) { | |
| this.state = {...initialState} | |
| this.prevState = {...initialState} | |
| }, | |
| getState() { | |
| const copy = {...this.state}; | |
| return { | |
| ...copy | |
| } | |
| }, | |
| getPrevState() { | |
| const copy = {...this.prevState}; | |
| return { | |
| ...copy | |
| } | |
| }, | |
| mergeState(newValues={}) { | |
| this.prevState = { ...this.state } | |
| this.state = { | |
| ...this.state, | |
| ...newValues | |
| }; | |
| }, | |
| mergeNodeInCollection(collectionName="", nodeId="", newValues={}) { | |
| this.prevState = { ...this.state } | |
| var newState = { ...this.state }; | |
| var collection = {...this.state[collectionName]} | |
| var newNode = { | |
| ...collection[nodeId], | |
| ...newValues | |
| }; | |
| collection[nodeId] = {...newNode} | |
| newState[collectionName] = {...collection} | |
| this.state = { | |
| ...newState | |
| } | |
| }, | |
| removeNodeInCollection(collectionName="", nodeId="") { | |
| this.prevState = { ...this.state } | |
| var newState = { ...this.state }; | |
| var collection = {...this.state[collectionName]} | |
| delete collection[nodeId]; | |
| newState[collectionName] = {...collection} | |
| this.state = { | |
| ...newState | |
| } | |
| } | |
| }; | |
| const Positions = { | |
| Stand: [0, 0], | |
| StepOff: [32, 0], | |
| Run1: [0, 32], | |
| Run2: [32, 32], | |
| Run3: [64, 32], | |
| Jump: [0, 64], | |
| //Left | |
| Left_Stand: [0, 96], | |
| Left_StepOff: [32, 96], | |
| Left_Run1: [0, 128], | |
| Left_Run2: [32, 128], | |
| Left_Run3: [64, 128], | |
| Left_Jump: [0, 162], | |
| }; | |
| //Frame Arrays | |
| const MegaManPoses = { | |
| Stand: [Positions.Stand], | |
| StepOff: [Positions.StepOff], | |
| Run: [ | |
| Positions.Run1, Positions.Run2, Positions.Run3, Positions.Run2 | |
| ], | |
| Jump: [Positions.Jump], | |
| //Left | |
| Left_Stand: [Positions.Left_Stand], | |
| Left_StepOff: [Positions.Left_StepOff], | |
| Left_Run: [ | |
| Positions.Left_Run1, Positions.Left_Run2, Positions.Left_Run3, Positions.Left_Run2 | |
| ], | |
| Left_Jump: [Positions.Left_Jump], | |
| } | |
| function mergeState(newValues={}) { | |
| Data.mergeState(newValues) | |
| } | |
| function isTouching(my,other) { | |
| return my.x + my.width > other.x && | |
| my.x <= other.x + other.width && | |
| my.y + my.height > other.y && | |
| my.y <= other.y + other.height; | |
| } | |
| function getSolidSurface(my, others=[]) { | |
| //Create 1 px sliver as my underline; | |
| //const underlineModel = { | |
| // height:1, | |
| // width: my.width, | |
| // y: my.y + my.height, | |
| // x: my.x | |
| //}; | |
| var touchingModel = null; //Assume False | |
| others.forEach(otherModel => { | |
| if (touchingModel) { | |
| return; //Dont rerun if we already have a match | |
| } | |
| if (isTouching( my, otherModel)) { | |
| touchingModel = {...otherModel}; | |
| } | |
| }); | |
| return touchingModel; | |
| } | |
| function getSolidSurfaceDown(my, others=[]) { | |
| //Create 1 px sliver as my underline; | |
| const underlineModel = { | |
| height:1, | |
| width: my.width, | |
| y: my.y + my.height, | |
| x: my.x | |
| }; | |
| var touchingModel = null; //Assume False | |
| others.forEach(otherModel => { | |
| if (touchingModel) { | |
| return; //Dont rerun if we already have a match | |
| } | |
| if (isTouching( underlineModel, otherModel)) { | |
| touchingModel = {...otherModel}; | |
| } | |
| }); | |
| return touchingModel; | |
| } | |
| function drawCharacter(ctx, state, assets) { | |
| const characterWidth = state.characterWidth; | |
| const characterHeight = state.characterHeight; | |
| ctx.beginPath(); | |
| /* Debug Rectangle */ /* This is the hitbox */ | |
| //ctx.fillStyle = "#fff"; | |
| //ctx.fillRect( | |
| // state.characterX, state.characterY, | |
| // characterWidth, characterHeight | |
| //); | |
| const currentPose = state.characterPose; | |
| const activeFrame = currentPose[state.characterFrame] || currentPose[0]; | |
| ctx.drawImage( | |
| assets.mm, | |
| activeFrame[0], activeFrame[1], //Where in the spritesheet x/y | |
| 32,32, | |
| state.characterX - 5, state.characterY - 4, //nudging where the drawing of the sprite is | |
| 32,32 | |
| ); | |
| } | |
| function drawWalls(ctx, state) { | |
| state.walls.forEach(wall => { | |
| ctx.beginPath(); | |
| ctx.fillStyle = "#222"; | |
| ctx.fillRect(wall.x, wall.y, wall.width, wall.height); | |
| }); | |
| } | |
| function draw(canvas, ctx, state, assets) { | |
| drawSky(ctx, state); | |
| drawWalls(ctx, state); | |
| drawCharacter(ctx, state, assets); | |
| } | |
| function drawSky(ctx, state) { | |
| ctx.beginPath(); | |
| ctx.fillStyle = "#4AB5E2"; | |
| ctx.fillRect(0,0, state.canvasWidth, state.canvasHeight); | |
| } | |
| function runSteps(state, prevState, frameCount, dt) { | |
| //bulletSteps(state); | |
| playerMovement(state, prevState, frameCount, dt); | |
| } | |
| function runInits(state) { | |
| /* Run all of the "kickoff" processses, like keyboard bindings and intervals */ | |
| bindKeyboardListeners(); | |
| } | |
| function playerMovement(state, prevState, frameCount, dt) { | |
| let isFacingLeft = state.isFacingLeft; | |
| let nextCharacterX = state.characterX; | |
| let nextCharacterY = state.characterY; | |
| let inAir = state.inAir; | |
| let isAbleToJump = state.isAbleToJump; | |
| const downUnit = 5; | |
| const nextDownFrame = { | |
| x: nextCharacterX, | |
| y: nextCharacterY + (downUnit), | |
| width: state.characterWidth, | |
| height: state.characterHeight | |
| }; | |
| const surfaceCandidate = getSolidSurface(nextDownFrame, state.walls); | |
| const surface = (surfaceCandidate && surfaceCandidate.y >= nextCharacterY) ? surfaceCandidate : null; | |
| if (!surface) { | |
| inAir = true; | |
| nextCharacterY += downUnit | |
| isAbleToJump = false; | |
| } | |
| if (surface) { | |
| isAbleToJump = true; | |
| } | |
| if (surface && inAir) { | |
| window.landingSfx.play(); | |
| inAir = false; | |
| isAbleToJump = true; | |
| //Correcting you | |
| nextCharacterY = surface.y - state.characterHeight; | |
| } | |
| //Horizontal Movement | |
| const xMovementUnit = Math.round(dt * 130); | |
| if (state.isKeyboardLeftPressed) { | |
| const leftUnit = xMovementUnit; | |
| const nextLeftFrame = { | |
| x: nextCharacterX - leftUnit, | |
| y: nextCharacterY, | |
| width: state.characterWidth, | |
| height: state.characterHeight | |
| }; | |
| const leftSurface = getSolidSurface(nextLeftFrame, state.walls); | |
| if (!leftSurface) { | |
| nextCharacterX -= leftUnit; | |
| } | |
| isFacingLeft = true; | |
| } | |
| if (state.isKeyboardRightPressed) { | |
| const rightUnit = xMovementUnit; | |
| const nextRightFrame = { | |
| x: nextCharacterX + rightUnit, | |
| y: nextCharacterY, | |
| width: state.characterWidth, | |
| height: state.characterHeight | |
| }; | |
| const rightSurface = getSolidSurface(nextRightFrame, state.walls); | |
| if (!rightSurface) { | |
| nextCharacterX += rightUnit; | |
| } | |
| isFacingLeft = false; | |
| } | |
| /////////////////////////////////// | |
| let verticalBoost = state.verticalBoost; | |
| /* VERTICAL BOOST */ | |
| if (verticalBoost < 0) { | |
| const unit = 9; | |
| const nextUpY = nextCharacterY -= unit; | |
| //CHECK FOR CEILINGS | |
| const nextUpFrame = { | |
| x: nextCharacterX, | |
| y: nextUpY, | |
| width: state.characterWidth, | |
| height: state.characterHeight | |
| }; | |
| const surfaceUp = getSolidSurface(nextUpFrame, state.walls); | |
| if (!surfaceUp) { | |
| nextCharacterY = nextUpY; | |
| //move boost back towards 0 | |
| verticalBoost = state.verticalBoost + unit; | |
| } else { | |
| verticalBoost = 0; //Kill the boost. Hit your head | |
| } | |
| } | |
| ////ANIMATION | |
| //Change active frame | |
| let nextFrame = state.characterFrame; | |
| if (frameCount % 8 == 0) { | |
| nextFrame = (nextFrame <= 2) ? nextFrame + 1 : 0; | |
| } | |
| //Revive! | |
| if (nextCharacterY > 300 + 50) { | |
| nextCharacterY = -50 | |
| } | |
| /* Merge all state changes */ | |
| mergeState({ | |
| characterFrame: nextFrame, | |
| characterPose: getCharacterPose({ //Sprite | |
| ...state, | |
| inAir:inAir | |
| }), | |
| characterX: nextCharacterX, | |
| characterY: nextCharacterY, | |
| verticalBoost: verticalBoost, | |
| isFacingLeft: isFacingLeft, | |
| isAbleToJump: isAbleToJump, | |
| inAir: inAir | |
| }); | |
| } | |
| function bindKeyboardListeners() { | |
| var jumpSafe = true; | |
| document.addEventListener('keydown', function (e) { | |
| if (e.which == 37) { | |
| mergeState({ | |
| isKeyboardLeftPressed: true | |
| }); | |
| } | |
| if (e.which == 39) { | |
| mergeState({ | |
| isKeyboardRightPressed: true | |
| }); | |
| } | |
| //Jump! | |
| if (e.which == 38) { | |
| if ( Data.getState().isAbleToJump ) { | |
| if (jumpSafe) { | |
| jumpSafe = false; | |
| mergeState({ | |
| isAbleToJump: false, | |
| verticalBoost: -170 | |
| }); | |
| } | |
| } | |
| } | |
| }, false); | |
| document.addEventListener('keyup', function (e) { | |
| if (e.which == 37) { | |
| mergeState({ | |
| isKeyboardLeftPressed: false | |
| }); | |
| } | |
| if (e.which == 39) { | |
| mergeState({ | |
| isKeyboardRightPressed: false | |
| }); | |
| } | |
| //Release Jump! | |
| if (e.which == 38) { | |
| jumpSafe = true; | |
| mergeState({ | |
| verticalBoost: 0 | |
| }); | |
| } | |
| }, false); | |
| } | |
| //////////////////////////////////////////////////// | |
| function getCharacterPose(state) { | |
| const isLeft = state.isFacingLeft; | |
| if (state.inAir) { | |
| return isLeft ? MegaManPoses.Left_Jump : MegaManPoses.Jump; | |
| } | |
| if (state.isKeyboardLeftPressed || state.isKeyboardRightPressed) { | |
| return isLeft ? MegaManPoses.Left_Run : MegaManPoses.Run; | |
| } | |
| return isLeft ? MegaManPoses.Left_Stand : MegaManPoses.Stand; | |
| } | |
| ///////////////////////////////////////////////// now to use all this stuff! | |
| //Cache references to canvas and context | |
| var canvas = document.getElementById("js-canvas"); | |
| var ctx = canvas.getContext("2d"); | |
| //Init the app | |
| Data.init(initialState, canvas, ctx); | |
| //Set up assets | |
| let assets = { | |
| mm: new Image() | |
| }; | |
| assets.mm.src = `https://s3-us-west-2.amazonaws.com/s.cdpn.io/21542/mm-blue-sprites.png`; | |
| /* Draw Loop */ | |
| var frameCount=1; | |
| var lastTime; | |
| var step = function() { | |
| var now = Date.now(); | |
| var dt = (now - lastTime) / 1000.0; | |
| const state = Data.getState(); | |
| const prevState = Data.getPrevState(); | |
| //Draw the state | |
| draw(canvas, ctx, state, assets); | |
| //Run Steps - adjust state for next pass | |
| runSteps(state, prevState, frameCount, dt); | |
| //Track frame count for character animations | |
| frameCount += 1; | |
| if (frameCount > 64) { frameCount = 1} | |
| lastTime = now; | |
| requestAnimationFrame(step) | |
| }; | |
| assets.mm.onload = function() { | |
| const state = Data.getState(); | |
| /* Inits */ | |
| runInits(state); | |
| requestAnimationFrame(step); | |
| }; |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/howler/1.1.29/howler.min.js"></script> |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-size: 16px; | |
| padding: 3em 1em; | |
| font-family: monospace; | |
| } | |
| p { | |
| text-align: center; | |
| } | |
| canvas { | |
| margin: 0 auto; | |
| display: block; | |
| border-radius: 3px; | |
| ; | |
| } |