Reputation: 1747
Im writing a Tomb of the Mask game clone in Canvas for an assignment, and I wanted to create a small animation so that when my character "Snaps" to the next wall, it doesnt just "teleport" like it does now.
See here for a live review: https://codepen.io/SkylerSpark/pen/GRpdzBZ
Currently I have a big keydown event and a switch case that detects for any of the 4 arrow keys, this is an example of one of the case statements:
case "ArrowLeft":
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
while (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
playerCoords[0]--;
}
}
Ill split up those map statements for a better understanding:
map[playerCoords[1]][playerCoords[0] - 1] != 1
map[] - Main Map Data (1s and 0s that determine the game layout)
playerCoords[0 / 1] - Location of the Player
map[ pc[1] ] (going to get the sub array of map[playerCoords[1]]) > [pc[0] - 1] (-1 to look for the block to the left of the player)
then Im selecting all of that into one statement and detecting if its equal to 1 (1 is a brick block) to see if the player should MOVE or NOT MOVE.
Anyways, I have an animationFrame running on my player location at ALL TIMES so that if any adjustments are made, it will show them.
The while loops I use to send the player to the opposite wall (rather than just moving 1 block to the left or right, it needs to work JUST like TotM) are just immediately sending the results. I want it to quickly move all those blocks 1 by 1 but barely noticable...
Is it possible I can add some kind of "delay" inside the while loops so it can move 1 block, then wait 10 milliseconds, and then the next, so on so fourth?
Full Game and Code:
View in FULL PAGE or it wont work correctly..
const cvs = document.querySelector(".bastione"),
ctx = cvs.getContext("2d");
const cvs2 = document.querySelector(".basPlayer"),
ctx2 = cvs2.getContext("2d");
ctx.imageSmoothingEnabled = ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled = false;
ctx2.imageSmoothingEnabled = ctx2.mozImageSmoothingEnabled = ctx2.webkitImageSmoothingEnabled = false;
function loadImage(src, callback) {
var img = new Image();
img.onload = callback;
img.setAttribute("crossorigin", "anonymous");
img.src = src;
return img;
}
function ran(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
const map = [
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
];
const drawEnvironment = {
init: () => {
drawEnvironment.renderBack();
},
renderBack: () => {
let cx = 0, cy = 0;
map.forEach(e => {
for (var i = 0; i < e.length; i++) {
if (e[i] == 1) {
let v = ran(0, 10);
if (v > 0 && v < 8) {
ctx.drawImage(spriteImage, 0, 0, 32, 32, cx, cy, 32, 32);
} else {
ctx.drawImage(spriteImage, 32, 0, 32, 32, cx, cy, 32, 32);
}
cx += 32;
} else if (e[i] == 2 || e[i] == 3) {
ctx.drawImage(spriteImage, 64, 64, 32, 32, cx, cy, 32, 32);
cx += 32;
} else {
let v = ran(0, 10);
if (v > 0 && v < 5) {
ctx.drawImage(spriteImage, 128, 64, 32, 32, cx, cy, 32, 32);
if (v == 10) {
ctx.drawImage(spriteImage, 128, 32, 32, 32, cx, cy, 32, 32);
}
} else {
ctx.drawImage(spriteImage, 128, 32, 32, 32, cx, cy, 32, 32);
}
cx += 32;
}
}
cx = 0;
cy += 32;
});
ctx.drawImage(spriteImage, 0, 0, 32, 32, 0, 0, 32, 32);
}
};
let playerCoords = [1, 1];
const drawPlayer = {
init: () => {
drawPlayer.playerLoc();
drawPlayer.playerMove();
},
playerLoc: () => {
ctx2.drawImage(spriteImage, 0, 64, 32, 32, playerCoords[0] * 32, playerCoords[1] * 32, 32, 32);
window.requestAnimationFrame(drawPlayer.playerLoc);
},
playerMove: () => {
document.addEventListener("keydown", function(event) {
ctx2.clearRect(0, 0, cvs2.width, cvs2.height);
event.preventDefault();
const key = event.key;
switch (key) {
case "ArrowLeft":
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
while (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
playerCoords[0]--;
}
}
break;
case "ArrowRight":
if (map[playerCoords[1]][playerCoords[0] + 1] != 1) {
while (map[playerCoords[1]][playerCoords[0] + 1] != 1) {
playerCoords[0]++;
}
}
break;
case "ArrowUp":
if (map[playerCoords[1] - 1][playerCoords[0]] != 1) {
while (map[playerCoords[1] - 1][playerCoords[0]] != 1) {
playerCoords[1]--;
}
}
break;
case "ArrowDown":
if (map[playerCoords[1] + 1][playerCoords[0]] != 1) {
while (map[playerCoords[1] + 1][playerCoords[0]] != 1) {
playerCoords[1]++;
}
}
break;
}
});
}
}
const spriteImage = loadImage(
"https://cdn.jsdelivr.net/gh/FunctFlow/Bastione-Game@1d0514c968a737061916ae5e160b20eaf3a6b8b4/Sprites/Bastione_Sprites.png",
() => {
drawEnvironment.init();
drawPlayer.init();
}
);
* {
box-sizing: border-box;
overflow: hidden;
}
body {
text-align: center;
background: black;
}
canvas {
display: inline-block;
}
.basPlayer {
position: absolute;
margin-left: -1024px;
}
<canvas class=bastione width=1024 height=512></canvas>
<canvas class=basPlayer width=1024 height=512></canvas>
View in FULL PAGE or it wont work correctly..
Upvotes: 1
Views: 191
Reputation: 1747
Thanks to ViktorW, gman and VLAZ for the help and answers, I came up with a solution though:
so if you put a "run object" into the project, and have 4 sub variables with true/false, and change them depending on which key you press, you can make it work perfectly.
I followed ViktorW's answer and applied this idea, plus I just made a simple interval instead of the logic Viktor used:
so outside of my movement function, I have the run
variable
let run = {
l: true,
r: true,
u: true,
d: true
}
theres variables for the 4 directions (Left Right Up Down)
Then just add the logic into the movement function + the interval and ITS logic:
case "ArrowLeft":
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
if (run.l == true) { // Detect for direction
var lInterval = setInterval(() => {
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
playerCoords[0]--;
run.r = run.u = run.d = false; // Set all other directions false
} else {
clearInterval(lInterval);
run.r = run.u = run.d = true; // Set all other directions true when done moving
}
}, 10);
}
}
break;
This PERFECTLY prevents the opposing movement of of the block if you use an interval to achieve the animation.
Check it out live here, use arrow keys to control: Tomb of the Mask Clone
Upvotes: 0
Reputation:
This is BIG TOPIC
The normal thing to do would be for each thing (player, monster, etc..) to give them some kind of update function
const allTheThings = [];
function loop() {
for (const thing of things) {
thing.update();
}
requestAnimationFrame(loop);
}
In each of those update
functions you would do whatever is appropriate for that thing doing only what is need at this moment. So for example the player might have a update function like this
class Player() {
constructor() {
this.coords = [1, 1];
this.delta = [0, 0];
}
update() {
if (this.waiting) {
this.waiting -= 1;
} else if (this.moving) {
this.coords[0] = this.delta[0];
this.coords[1] = this.delta[1];
this.waiting = 10;
} else {
// check the keys
// and set this.delta and moving appropriately
}
}
}
Then you can make a player and add it to this array of allTheThings
const player = new Player();
allTheThings.push(player);
Note that is way over simplified. Most games don't directly update in an object like that. Instead, just like allTheThings
calls update for each thing, a thing itself might have a list of subthings (components) each of which also has an update function. A thing, or a GameThing, is a collection of these components.
Further, there are all kinds of ways to help organize what those update functions do. In the example above there were 2 flags moving
and waiting
but as the game gets more and more complicated there get to be too many flags so people have come up with things like Finite State Machines and Coroutines and may other techniques to help make that stuff simpler.
Maybe not useful but here is an article that uses some of these techniques.
Upvotes: 1
Reputation: 1149
You can use setInterval
to create a loop which runs at a specific frequency.
case "ArrowLeft":
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
let interval = setInterval(() => {
if (map[playerCoords[1]][playerCoords[0] - 1] != 1) {
// Stop the interval, and do nothing.
clearInterval(interval)
return
}
playerCoords[0]--;
}, 100) // <- 100 here is delay in milliseconds
}
This code is now running asynchronously
. This means that while we are waiting for the delay, other code can run. If we are not careful, this can introduce bugs. For example: You need to think about what would happen if the player presses arrowRight while the arrowLeft is still being processed. If you do not fix that case, the character will move both left and right at the same time, which would mean that he would never hit a wall and you would be stuck in an endless loop.
Upvotes: 1