So, I'm making a game on HTML5 canvas. It's a top down shooter, and I need to create a bullet every time you click to make the character shoot.
Initially, I just prevented the player from firing another bullet until it went out of bounds or it hit an enemy, as seen here. This worked percetly, but of course, makes for uninteresting gameplay.
Then, I began researching about JS classes, and I thought that it would be the key to the problem. I created a bullet class, and moved all the logic for the bullet to the class. Then, I created an instance of it, and called it in other parts of the code to execute its logic. This worked exactly as it did before, which is good, because it meant I could translate the thing I had before to a class, but it had a similar issue.
This is how the class is defined:
class bullet{
constructor(_img, _piercing){
this.bulletPic = document.createElement("img");
this.img = this.bulletPic.src = _img;
this.piercing = _piercing;
}
shoot(){
this.bulletAngle = playerAngle;
this.bulletX = playerX;
this.bulletY = playerY;
bulletShot = true;
shots = 0;
}
draw(){
canvasContext.save();
canvasContext.translate(this.bulletX, this.bulletY);
canvasContext.rotate(this.bulletAngle);
canvasContext.drawImage(this.bulletPic, -this.bulletPic.width / 2, -this.bulletPic.height / 2);
canvasContext.restore();
if(bulletShot){
this.bulletX += Math.sin(this.bulletAngle) * BULLET_SPEED;
this.bulletY -= Math.cos(this.bulletAngle) * BULLET_SPEED;
}
}
}
And here is the object definition:
let bullet1 = new bullet("Textures/player.png", true);
If I want to shoot another bullet at the same time, I need to have already defined a new instance of the bullet class, is there any way for me to define a new instance every time I click?
Edit: The shoot and draw methods are called in another file that follow logic that's not shown here. Mainly what this other code does, is detect when it hits an enemy or when it goes out of bounds to set "bulletShot" to false, that makes it "despawn", and I can shoot another bullet. This is part of the 1 bullet at a time limitation I'm trying to remove here, but that can go once this central issue is fixed.
If I understand your situation, you could use a function that returns a new class:
function bulletFactory( className ) {
return new className();
}
If you want to achieve that there could be several bullets in "mid-air", after a series of fast consecutive clicks, then create an array of bullets. You would initialise that array like this:
const bullets = Array({length: ammo}, () => new Bullet());
ammo would be the number of bullets that the user can shoot in total.
NB: I simplified the call of the constructor. Add the arguments you want to pass. Secondly, it is common practice to start class names with a capital.
Then add a state property in the Bullet instances that indicates whether the bullet is:
Hidden: it is not visible yet, but part of the total ammunition that can still be used in the future
Ready: it is the one bullet that is visible at the start location, ready to be fired by the user
Shot: a bullet that has been shot and is currently flying through the game area
At first this state is "hidden":
constructor(_img, _piercing){
this.state = "hidden";
// ...
}
draw() {
if (this.state === "hidden") return; // Don't draw bullets that are not available
// ...
}
Then at the start of the game, make one bullet visible (where it should be clicked):
bullets[0].state = "ready"; // From now on it will be drawn when `draw()` is called
In the click handler do the following:
// Fire the bullet the user clicked on:
bullets.find(bullet => bullet.state === "ready").shoot(playerAngle, playerX, playerY);
// See if there is a next bullet remaining in the user's ammo:
const nextBullet = bullets.find(bullet => bullet.state === "hidden");
if (nextBullet) nextBullet.state = "ready"; // Otherwise ammo is depleted.
The shoot method should not rely on global variables, but get the necessary external info as arguments:
shoot(playerAngle, playerX, playerY) {
this.bulletAngle = playerAngle;
this.bulletX = playerX;
this.bulletY = playerY;
this.state = "shot";
}
Don't use global variables inside your class methods (shot, ammo,...). Instead use arguments or other instance properties.
The draw method should also work with that state:
draw() {
if (this.state === "hidden") return; // Don't draw bullets that are not available
// ...
if(this.state === "shot") {
this.bulletX += Math.sin(this.bulletAngle) * BULLET_SPEED;
this.bulletY -= Math.cos(this.bulletAngle) * BULLET_SPEED;
}
}
In your animation loop, you should call draw on all bullets. Something like:
bullets.forEach(bullet => bullet.draw());
I did not see any code for when a bullet has left the game area, either by hitting something or just flying out of range. In such case the bullet should be removed from the bullets array to avoid that the draw method keeps drawing things without (visual) significance.
Here is how you could delete a specific bullet:
function deleteBullet(bullet) {
const i = bullets.indexOf(bullet);
if (i > -1) bullets.splice(i, 1);
}
I hope this gets you going on your project.
I ended up making an array that contains multiple instances of the class. I defined a variable that I used as a limit and then set up a for statement to create all the objects, then, I can call them using the array name and the position.
for(var i = 0; i < arraySize; i++){
arrayName[i] = new className(parameters);
}
Examples of usage:
arrayName[5].method();
Related
I've got what seems like a very simple problem but I'm having some problems fixing it. I'm trying to create an interactive cat object on my page with Javascript that basically performs the following expected behavior:
When the page first loads, the cat is laying sleeping in the middle of the screen (cat-sleeping.gif)
When you move the pointer (customized to look like a mouse) a little bit closer to the cat, it sits up and starts grooming itself (cat-sitting.gif)
When you move the pointer even closer, the cat starts chasing after the cursor (cat-chasing.gif)
To handle detecting the mouse proximity and the chase behaviour, I'm using a loop that's being called from window.requestAnimationFrame after an initial setup call. The chase + mouse proximity are working as expected, but the changing animation states are not. Although the image changes as expected when the cat is activated, the animated gif becomes a static image. If it matters, I'm using Google Chrome to test things out.
Here's a snippet of the loop (note this function isn't completely finished, but everything that should be working so far works, except the animation)
function loop() {
const { pointerEvent, pointerStatic, catActive } = state;
const { cat, cursor } = settings;
// Calculate distance between cat and pointer
const distance = Math.sqrt(
Math.pow(pointerEvent.x - cat.element.offsetLeft, 2) +
Math.pow(pointerEvent.y - cat.element.offsetTop, 2)
);
// Normalize the distance
const distanceNorm = scale(distance, 0, 1000);
// Activate the cat after the pointer gets close enough
if (distanceNorm < 0.1) {
updateState({ catActive: true });
}
// Flip the cursor left or right depending on the direction of movement
if (pointerEvent.movementX < 0) {
cursor.element.style.transform = "scaleX(-1)";
} else {
cursor.element.style.transform = "scaleX(1)";
}
// Make the cat turn from left to right when the pointer is moving
if (pointerEvent.x < cat.element.offsetLeft + cat.width / 2) {
cat.element.style.transform = "scaleX(-1)";
} else {
cat.element.style.transform = "scaleX(1)";
}
// If the cat is active, chase the pointer
if (catActive) {
cat.element.src = "cat-walk.gif";
cat.element.style.left = `${toAbsolute(
distanceNorm,
cat.element.offsetLeft,
pointerEvent.x - 80
)}px`;
cat.element.style.top = `${toAbsolute(
distanceNorm,
cat.element.offsetTop,
pointerEvent.y - 35
)}px`;
}
window.requestAnimationFrame(loop);
}
Changing my state function fixed this - I made a separate change state function specifically for the cat, since I realized that state was otherwise being updated every frame as the pointer moved due to sharing data with the PointerElement event. However, I'm not 100% sure why this actually worked - to make it function, I needed to directly access catActive from my catState object instead of using its destructured variant. I'm posting this as an answer so that I can share the code, but I'd like to understand why this actually works so that I learn instead of surviving by trial and error!
function updateCatState(newState) {
let { catActive, cat, catForm } = catState;
catState = { ...catState, ...newState };
if (catState.catActive && cat.element.src !== catForm[1]) {
console.log("catActive");
cat.element.src = catForm[1];
} else {
cat.element.src = catForm[0];
}
}
I am trying to animate a line two lines along a path, one then the other. Basically it will look like one line being drawn, stopping at a point, then another line being drawn somewhere else. So far I have come across promises and callbacks to achieve this, but being a javascript newbie this is confusing
Current animate function:
/*
* Animation function draws a line between every point
*/
var animate = function(p){
return new Promise(function(resolve) {
t = 1;
var runAnimation = function(){
if(t<p.length-1){
context.beginPath();
context.moveTo(p[t-1].x,p[t-1].y);
context.lineTo(p[t].x,p[t].y);
context.stroke();
t++;
requestAnimationFrame(function(){runAnimation()});
} else {
resolve()
}
};
runAnimation();
});
}
Current call to animate function:
animate(points).then(animate(secondary_points));
The points are similar to:
var points = [{x:100, y:200}];
And the paths the lines need to follow are just the multiple coordinates inside points and secondary_points
Ive tried many solutions on SO that were similar, but small differences cause me to either mess up or not understand the solution. The biggest issue I seem to have is calling the SAME animate function, with that animate function needing to be run on different parameters.
Without this solution, using
animate(points);
animate(secondary_points);
the lines are drawn somewhat at the same time, but the result is actually just randomly placed dots along the path instead of smooth lines, I assume because both are running at the same time.
How would I go about fixing this so that one line is drawn along path1 and then the second line is drawn along path2?
It is probably a simple solution, but Ive worked with JS for 3 days and my head is still spinning from getting used to some of the syntax of the old code Ive had to fix
Thank you
EDIT:
The full flow of the animation is as follows:
I have a php file that contains 2 canvases, each containing an image of a map. The php file has a couple <script/> tags, one of which calls the js script I am writing the animation on via drawPath(source,destination,true) or drawPath(source,destination,false)
The drawPath function uses the boolean to determine which canvas to get the context for, and then draw on the path from point A to point B via finding the path and creating the points mentioned above, then drawing using animate(). There are a couple breaks in the maps that require separate lines, which prompted my original question. I was able to fix that thanks to suggestions, but now I am having a larger issue.
If I need to go from point A on map A to point B on map B, ie
drawPath(source, end_point_of_map_A, true); is called then
drawPath(start_point_of_map_B, destination, false);, the lines are drawn only on one map, and they are similar to before where they are 1. random and 2. incomplete/only dots
I am assuming this is due to the animation again, because it worked when just drawing the lines statically, and each animation works when going from point A to B on a single map
Any help is appreciated!
Edit:
DrawPath()
function drawPath(source, desti, flag) {
/*
* Define context
*/
//lower
if(!flag){
var c = document.getElementById("myCanvas");
context = c.getContext("2d");
//upper
} else {
var cUpr = document.getElementById("myCanvasUpr");
context = cUpr.getContext("2d");
}
/*
* Clear the variables
*/
points = [];
secondary_points = [];
vertices = [];
secondary_vertices = [];
t = 1;
done = false;
//check for invalid locations
if (source != "" && desti != "") {
context.lineCap = 'round';
context.beginPath();
/*
* Get the coordinates from source and destination strings
*/
var src = dict[source];
var dst = dict[desti];
/*
* Get the point number of the point on the path that the source and destination connect to
*/
var begin = point_num[source];
var finish = point_num[desti];
/*
* Draw the green and red starting/ending circles (green is start, red is end)
*/
context.beginPath();
context.arc(src[0], src[1], 8, 0, 2 * Math.PI);
context.fillStyle = 'green';
context.fill();
context.beginPath();
context.arc(dst[0], dst[1], 6, 0, 2 * Math.PI);
context.fillStyle = 'red';
context.fill();
/*
* Call the function that draws the entire path
*/
draw_segments(begin, finish, src, dst, flag);
//window.alert(JSON.stringify(vertices, null, 4))
/*
* Edit what the line looks like
*/
context.lineWidth = 5;
context.strokeStyle = "#ff0000";
context.stroke();
}
}
A nice way to handle this is to put your lines into a an array where each element is a set of points of the line. Then you can call reduce() on that triggering each promise in turn. reduce() takes a little getting used to if you're new to javascript, but it basically takes each element of the array c in this case, does something and that something becomes the next a. You start the whole thing off with a resolve promise which will be the initial a. The promise chain will be returned by reduce to you can tack on a final then to know when the whole thing is finished.
For example:
let canvas = document.getElementById('canvas')
let context = canvas.getContext('2d');
var animate = function(p){
return new Promise(function(resolve) {
t = 1;
var runAnimation = function(){
if(t<p.length-1){
context.beginPath();
context.moveTo(p[t-1].x,p[t-1].y);
context.lineTo(p[t].x,p[t].y);
context.stroke();
t++;
requestAnimationFrame(function(){runAnimation()});
} else {
resolve()
}
};
runAnimation();
});
}
// make some points:
let points = Array.from({length: 200}, (_,i) => ({x:i+1, y:i+2}))
let points2 = Array.from({length: 200}, (_,i) => ({x:300-i, y:i+2}))
let points3 = Array.from({length: 200}, (_,i) => ({x:i*2, y:100+100*Math.sin(i/10)}))
// create an array holding each set
let sets = [points, points2, points3]
// use reduce to call each in sequence returning the promise each time
sets.reduce((a, c) => a.then(() => animate(c)), Promise.resolve())
.then(() => console.log("done"))
<canvas id="canvas" height="300" width="500"></canvas>
I am drawing points on a canvas with javascript, like so:
function addPointToMap(point) {
var pointRadius = (document.getElementById(point.canvasId).height * (2 / 66)) / 2;
var context = document.getElementById(point.canvasId).getContext("2d");
context.beginPath();
context.arc(point.x, point.y, pointRadius, 0, 2 * Math.PI);
context.fillStyle = "red";
context.fill();
}
and all the points I am drawing are stored in an array, pointMap. I want the user to only be able to draw one point if a checkbox is ticked, and draw many points if it is not ticked. A new point should override an old one. In order to do this, I have decided to add the new point to the array, and then remove the old one and refresh the canvas. The problem is that pointMap = pointMap.pop(); is returning an empty array. How do I get the most recent entry in an array and delete all the other entries? Here is what I have so far:
if (questionId == 41) {
if (pointMap.length == 1) {
//do nothing, user only has 1 point
} else {
console.log("PointMap: " + pointMap); //ex. returns [Point, Point] (Point is a custom class I wrote to store the point x and y values)
pointMap = pointMap.pop(); //this line does not work
console.log("PointMap: " + pointMap); //ex. returns []
refreshCanvas();
}
}
Where am I going wrong? can anyone steer me in the right direction?
pop returns the popped value, so pointMap = pointMap.pop() will replace your array reference with a point.
If you want to only have a single point in the array when the checkbox is checked, simply overwrite it:
if (checkboxIsChecked) {
// Only want one point, assign to index 0 (works whether the
// array already has a point or not)
pointMap[0] = theNewPoint;
} else {
// Want to allow multiple points, push the point onto the array
pointMap.push(theNewPoint);
}
If the user can check the checkbox while there are already values in pointMap, you'll want to remove all but the last one when they check it. In your event handler for the checkbox:
if (checkboxIsChecked && pointMap.length > 1) {
// Remove all entries except the last pushed one
pointMap.splice(0, pointMap.length - 1);
}
I use an HTML button to call a function :
<button id="attackButton" type="button" onclick="Enemy.prototype.fight(Test)"> Attaquer </button>
This function is :
Enemy.prototype.fight = function(Enemy)
{
}
This function is linked to these 2 variables :
var Enemy = function (damage, life)
{
this.damage = damage;
this.life= life;
};
var Test= new Enemy (7,20);
I would like to make something which will replace the parameter"Test" by "Test2" within the HTML Button so my button will now redirect to the same function while using a third variable :
var Test2= new Enemy (10,30);
I hope I was clear enough. Thank you. :)
EDIT :
To clarify : A player is fighting different enemies. Enemies have different variables (armor, life, damage etc...) The fight is a function and variables varies based on the parameter. The html button calls that function.
The player is going to make various actions within the game and sometimes he will fight. Sometimes he will not.
I would like to keep one single html button which always call the same function. But the parameter has to change each time I want it.
For instance :
If you take the left side, then you fight Enemy 1.
If you take the right side, then you fight Enemy 2.
Right now the full the full Enemy function is
var Enemy = function (damage, initiative, armor, lifeNow, life)
{
this.damage = damage;
this.initiative = initiative;
this.armor = armor;
this.lifeNow = lifeNow;
this.life = life;
};
and each time I want to add a different enemy I write this :
var enemy1= new Enemy (7, 9, 3, 15, 15);
var enemy2= new Enemy (50, 9, 3, 100, 100);
Hope it helps :p Thank you again
I'm going to have to make a couple of assumptions here. The main one is that you have a number of Enemy objects and you want to attack each one in sequence.
First, use an array, not sequentially named variables.
var enemies = [ new Enemy (7,20), new Enemy (10,30) ];
Second, track which enemy is next to be attacked:
var nextEnemy = 0;
Third, refactor your code to make the attack enemy logic a function of its own.
Attack the current next enemy and then work out which one should be attacked after that.
function attackNextEnemy() {
Enemy.prototype.fight(enemies[nextEnemy]);
nextEnemy++;
if (!enemies[nextEnemy]) {
nextEnemy = 0; // back to the top
}
}
You can then call that function from onclick.
There are various other things you could to that would be better practises (such as mentioned in dfsq's comment, using addEventListener instead of HTML attributes), and putting the nextEnemy variable inside a closure to keep it private, but a Stackoverflow answer isn't the place to go into that level of depth.
After the question was updated:
If you take the left side, then you fight Enemy 1. If you take the right side, then you fight Enemy 2.
In that case, you probably don't want to use an array. You are dealing with positions not sequences.
An object such as:
var enemies = {
left: new Enemy (7,20),
right: new Enemy (10,30)
};
… would make more sense.
Then you would need to change your attack to:
var sideThePlayerIsOn = "left";
and
Enemy.prototype.fight(enemies[sideThePlayerIsOn]);
as well as adding some logic to change the sideThePlayerIsOn variable based on whatever the player can do to change that.
You can extract the variable from the button click event into a kind of context like the game in my example and in this way you can change the enemies any time you want.
For example if the side is changed just call: changeEnemy(game1, enemy2);
var Enemy = function (damage, initiative, armor, lifeNow, life)
{
this.damage = damage;
this.initiative = initiative;
this.armor = armor;
this.lifeNow = lifeNow;
this.life = life;
};
Enemy.prototype.fight = function(enemy) {
alert('it works!' + enemy.damage);
}
var Game = function (enemy1, enemy2)
{
this.enemy1 = enemy1;
this.enemy2= enemy2;
this.fight = function () {
this.enemy1.fight(this.enemy2);
}
};
var enemy1= new Enemy (7, 9, 3, 15, 15);
var enemy2= new Enemy (50, 9, 3, 100, 100);
var enemy3= new Enemy (60, 9, 3, 100, 100);
var game1= new Game(enemy1, enemy2);
function changeEnemy(game, newEnemy) {
game.enemy2 = newEnemy;
}
changeEnemy(game1, enemy3);
<button id="attackButton" type="button" onclick="game1.fight();"> Attaquer </button>
I'm trying to trigger an event half-way through the progress (not time) of a transition. It sounds simple, but since the transition can have any curve it's quite tricky. In my particular case it's not going to be paused or anything so that consideration is out of the way.
(Simplified) essentially I could trigger an animation on a modifier like this:
function scaleModifierTo(stateModifier, scale, animationDuration) {
stateModifier.setTransform(
Transform.scale(scale, scale, scale),
{
duration: animationDuration,
curve: this.options.curve
}
);
}
When the interpolated state of the Transitionable hits 0.5 (half-way through) I want to trigger a function.
I haven't dug that deep behind in the source of famo.us yet, but maybe need to do something like
subclass something and add the possibility to listen when the state passes through a certain point?
reverse the curve defined and use a setTimeout (or try to find a proximity using a few iterations of the chosen curve algorithm (ew))
Is it possible to do this easily? What route should I go down?
I can think of a couple of ways to achieve such, and both lend to the use of Modifier over StateModifier. If you are new, and haven't really had the chance to explore the differences, Modifier consumes state from the transformFrom method which takes a function that returns a transform. This is where we can use our own Transitionable to supply state over the lifetime of our modifier.
To achieve what you wish, I used a Modifier with a basic transformFrom that will alter the X position of the surface based on the value of the Transitionable. I can then monitor the transitionable to determine when it is closest, or in my case greater than or equal to half of the final value. The prerender function will be called and checked on every tick of the engine, and is unbinded when we hit the target.
Here is that example..
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Modifier = require('famous/core/Modifier');
var Transform = require('famous/core/Transform');
var Transitionable = require('famous/transitions/Transitionable');
var SnapTransition = require('famous/transitions/SnapTransition');
Transitionable.registerMethod('snap',SnapTransition);
var snap = { method:'snap', period:1000, damping:0.6};
var context = Engine.createContext();
var surface = new Surface({
size:[200,200],
properties:{
backgroundColor:'green'
}
});
surface.trans = new Transitionable(0);
surface.mod = new Modifier();
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
context.add(surface.mod).add(surface);
function triggerTransform(newValue, transition) {
var prerender = function(){
var pos = surface.trans.get();
if (pos >= (newValue / 2.0)) {
// Do Something.. Emit event etc..
console.log("Hello at position: "+pos);
Engine.removeListener('prerender',prerender);
}
}
Engine.on('prerender',prerender);
surface.trans.halt();
surface.trans.set(newValue,transition);
}
surface.on('click',function(){ triggerTransform(400, snap); });
The downside of this example is the fact that you are querying the transitionable twice. An alternative is to add your transitionable check right in the transformFrom method. This could get a bit strange, but essentially we are modifying our transformFrom method until we hit our target value, then we revert back to the original transformFrom method.. triggerTransform would be defined as follows..
Hope this helps!
function triggerTransform(newValue, transition) {
surface.mod.transformFrom(function(){
pos = surface.trans.get()
if (pos >= newValue/2.0) {
// Do something
console.log("Hello from position: " + pos)
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
}
return Transform.translate(pos,0,0)
})
surface.trans.set(newValue,transition);
}
Thank you for your responses, especially #johntraver for the prerender event, I wasn't aware of the existence of that event.
I realised it made more sense that I should handle this logic together with my move animation, not the scale one. Then, I ended up using a (very hacky) way of accessing the current state of the transition and by defining a threshold in px I can trigger my function when needed.
/**
* Move view at index to a specified offset
* #param {Number} index
* #param {Number} xOffset xOffset to move to
* #param {Object} animation Animation properties
* #return void
*/
function moveView(index, xOffset, animation) {
var rectModifier = this._views[index].modifiers.rect;
var baseXOffset = rectModifier._transformState.translate.state[0];
// After how long movement is reflow needed?
// for the sake of this example I use half the distance of the animation
var moveThreshold = Math.abs(baseXOffset - xOffset)/2;
/**
* Callback function triggered on each animation frame to see if the view is now covering
* the opposite so we can trigger a reflow of the z index
* #return void
*/
var prerender = function() {
var numPixelsMoved = Math.abs(baseXOffset - rectModifier._transformState.translate.state[0]);
if (numPixelsMoved > moveThreshold) {
Engine.removeListener('prerender', prerender);
// trigger a method when this is reached
_reflowZIndex.call(this);
}
}.bind(this);
rectModifier.setTransform(
Transform.translate(xOffset, 0, 0),
animation,
function() {
Engine.removeListener('prerender', prerender);
}
);
Engine.on('prerender', prerender);
}
Obviously the ._transformState.translate.state[0] is a complete hack, but I couldn't figure out of getting this value in a clean way without adding my own Transitionable, which I don't want. If there is a cleaner way of finding the current state as a Number between 0.0-1.0 that would be ace; anyone knows of one?