Related
I am trying to take Dan Shiffman's prime spiral program and make it object oriented.
I am putting all variables into the constructor and making the functions into methods to encapsulate them.
But the OOP version of the code does not draw more than 1 triangle to the screen. I can't see why there should be a problem, as I have all the variables and functions included in the scope of the primeSpiral class.
Working code without classes
let x, y;
let step = 1;
let stepSize = 20;
let numSteps = 1;
let state = 0;
let turnCounter = 1;
let offset = 0;
function setup() {
createCanvas(900, 900);
background(0);
const cols = width / stepSize;
const rows = height / stepSize;
x = width / 2;
y = height / 2;
for (let i = 0; i < 500; i++) {
noStroke();
primeSpiral(20, 1)
primeSpiral(30, 200)
incrementStep();
}
}
function incrementStep() {
switch (state) {
case 0:
x += stepSize;
break;
case 1:
y -= stepSize;
break;
case 2:
x -= stepSize;
break;
case 3:
y += stepSize;
break;
}
if (step % numSteps == 0) {
state = (state + 1) % 4;
turnCounter++;
if (turnCounter % 2 == 0) {
numSteps++;
}
}
step++;
}
function primeSpiral(offset, color) {
if (!isPrime(step + offset)) {
//might put something here
} else {
let r = stepSize * 0.5;
fill(color, 99, 164);
push();
translate(x, y);
rotate(-PI / 4);
triangle(-r, +r, 0, -r, +r, +r);
pop();
}
}
function isPrime(value) {
if (value == 1) return false;
for (let i = 2; i <= sqrt(value); i++) {
if (value % i == 0) {
return false;
}
}
return true;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>
With classes:
let stepSize = 20;
function setup() {
createCanvas(900, 900);
background(0);
const cols = width / stepSize;
const rows = height / stepSize;
x = width / 2;
y = height / 2;
background(0);
prime = new PrimeSpiral(0, 0, 2, stepSize, 1, 0) //: x,y,offset,color
}
function draw() {
prime.walk();
}
class PrimeSpiral {
constructor(x, y, number) {
this.x = x;
this.y = y;
this.number = number;
this.stepSize = 20;
this.numSteps = 1;
this.state = 0;
this.turnCounter = 1;
}
walk() {
if (!this.isPrime(this.number)) {
console.log(this.succes);
} else {
let r = stepSize * 0.5;
fill(color, 99, 164);
push();
translate(x, y);
rotate(-PI / 4);
triangle(-r, +r, 0, -r, +r, +r);
pop();
this.incrementStep()
}
}
isPrime(value) {
if (value == 1) return false;
for (let i = 2; i <= Math.sqrt(value); i++) {
if (value % i == 0) {
return false;
}
}
return true;
}
incrementStep() {
switch (this.state) {
case 0:
this.x += this.stepSize;
break;
case 1:
this.y -= this.stepSize;
break;
case 2:
this.x -= this.stepSize;
break;
case 3:
this.y += this.stepSize;
break;
}
if (this.step % this.numSteps == 0) {
this.state = (this.state + 1) % 4;
this.turnCounter++;
if (this.turnCounter % 2 == 0) {
this.numSteps++;
}
}
this.step++;
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.js"></script>
Sometimes you need to slow down to go faster :)
My hunch is you might attempted to write the class all in one go, without slowing down and testing one function at a time to ensure no errors snuck through.
Here are some of the main errors:
you forgot to add the step property to the class (this.step = 1;) in the constructor. In incrementStep() this.step which is undefined gets incremented which results in NaN throwing the rest off (e.g. state, etc.)
you left stepSize mostly as global variable, but are also using this.stepSize: try to avoid ambiguity and use one or the other (I recommend using this.stepSize since it will allow multiple instances to have independent step sizes)
In walk you're still using translate(x, y); when you probably meant translate(this.x, this.y);
Here's a version with the above notes applied (and optionally no longer incrementing or drawing triangles if x,y go outside the screen bounds):
let spiral1;
let spiral2;
function setup() {
createCanvas(900, 900);
background(0);
noStroke();
// instantiate spirals here so width, height set
spiral1 = new PrimeSpiral(20, 1);
spiral2 = new PrimeSpiral(30, 200);
}
function draw(){
spiral1.walk();
spiral2.walk();
}
class PrimeSpiral{
constructor(offset, color){
this.x = width / 2;
this.y = height / 2;
this.step = 1;
this.stepSize = 20;
this.numSteps = 1;
this.state = 0;
this.turnCounter = 1;
this.offset = offset;
this.color = color;
}
incrementStep() {
switch (this.state) {
case 0:
this.x += this.stepSize;
break;
case 1:
this.y -= this.stepSize;
break;
case 2:
this.x -= this.stepSize;
break;
case 3:
this.y += this.stepSize;
break;
}
if (this.step % this.numSteps == 0) {
this.state = (this.state + 1) % 4;
this.turnCounter++;
if (this.turnCounter % 2 == 0) {
this.numSteps++;
}
}
this.step++;
}
walk(){
// optional, early exit function if we're offscreen
if(this.x < 0 || this.x > width) return;
if(this.y < 0 || this.y > height) return;
if (!isPrime(this.step + this.offset)) {
//console.log('not prime:', step + offset);
} else {
let r = this.stepSize * 0.5;
fill(this.color, 99, 164);
push();
translate(this.x, this.y);
rotate(-PI / 4);
triangle(-r, +r, 0, -r, +r, +r);
pop();
}
this.incrementStep();
}
}
function isPrime(value) {
if (value == 1) return false;
for (let i = 2; i <= sqrt(value); i++) {
if (value % i == 0) {
return false;
}
}
return true;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.1/p5.min.js"></script>
If all my NPCs are stationary, the collision detection works as expected. However, when they are moving, the objects move through each other. I don't understand the difference between the moving object and the stationary object. I am tracking the hitboxes of all the NPCs and I'm pretty sure they're accurate. Maybe someone has some insight. Thanks in advance.
function Hero(map, x, y, facing, image, chars, type) {
this.map = map;
this.x = x;
this.y = y;
this.width = map.tsize;
this.height = map.tsize;
this.chars = chars
// facing = R=0 U=1 L=2 D=3
this.facing = facing
this.image = Loader.getImage(image)
this.type = type
}
Hero.prototype.hitBox = function(type){
if (this.type === "hero"){
return {
left: this.x - this.width/2,
right: this.x + this.width/2 -1,
top: this.y + this.height/2,
bottom: this.y + this.height -1
}
} else if (this.type === "npc") {
return {
left: this.x - this.width/2,
right: this.x + this.width/2 -1,
top: this.y - this.height/2,
bottom: this.y + this.height -1
}
}
};
Hero.SPEED = 256; // pixels per second
Hero.prototype.move = function (delta, dirx, diry) {
// move hero
this.x += dirx * Hero.SPEED * delta;
this.y += diry * Hero.SPEED * delta;
// clamp values
var maxX = (this.map.cols-2) * this.map.tsize;
var maxY = (this.map.rows-2) * this.map.tsize;
this.x = Math.max(0, Math.min(this.x, maxX));
this.y = Math.max(0, Math.min(this.y, maxY));
// check if we walked into a non-walkable tile
this._collide(delta, dirx, diry);
};
Hero.prototype.objCollision = function (obj){
let objHitBox = obj.hitBox()
let heroHitBox = this.hitBox()
if (objHitBox.left < heroHitBox.right&&
objHitBox.right > heroHitBox.left &&
objHitBox.top < heroHitBox.bottom &&
objHitBox.bottom > heroHitBox.top) {
return true
} else {
return false
}
}
Hero.prototype._collide = function (delta, dirx, diry) {
let row, col;
// check for collisions on sprite sides
let collision =
this.map.isSolidTileAtXY(this.hitBox()["left"], this.hitBox()["top"]) ||
this.map.isSolidTileAtXY(this.hitBox()["right"], this.hitBox()["top"]) ||
this.map.isSolidTileAtXY(this.hitBox()["right"], this.hitBox()["bottom"]) ||
this.map.isSolidTileAtXY(this.hitBox()["left"], this.hitBox()["bottom"])
//loop through all hexes with NPCs
let objCollision = this.chars.all.reduce(function (res, obj) {
let tmp;
if (obj !== this) {
tmp = this.objCollision(obj)
}
else {
return false
}
return res || tmp;
}.bind(this), false)
if (!collision && !objCollision) { return; }
if (diry > 0) {
row = this.map.getRow(this.hitBox()["bottom"]);
this.y = -Hero.SPEED*delta + this.y
}
else if (diry < 0) {
row = this.map.getRow(this.hitBox()["top"]);
this.y = Hero.SPEED*delta + this.y
}
else if (dirx > 0) {
col = this.map.getCol(this.hitBox()["right"]);
this.x = -Hero.SPEED*delta+ this.x;
}
else if (dirx < 0) {
col = this.map.getCol(this.hitBox()["left"]);
this.x = Hero.SPEED*delta + this.x;
}
};
aiMove(delta) {
switch(this.facing){
case 0:
this.dirx= 1
this.diry= 0
break;
case 1:
this.dirx= 0
this.diry= -1
break;
case 2:
this.dirx= -1
this.diry= 0
break;
case 3:
this.dirx= 0
this.diry= 1
break;
}
if (!this.waiting && this.moveTimer < 1) {
//Random direction
this.facing = Math.floor(Math.random() * Math.floor(4))
//random time
this.restStepTimer();
} else if (!this.waiting && this.moveTimer > 0) {
//Move to preset direction
this.move(delta, this.dirx, this.diry)
//subtract timer
this.moveTimer--;
//if timer is empty, switch to waiting and reset timer.
if (this.moveTimer <= 0) {
this.waiting = true;
this.restStepTimer();
}
} else if (this.waiting && this.moveTimer > 0) {
//while waiting, we lower the timer.
this.moveTimer--;
//when the timer runs out, we flip the flag back to not waiting and start again.
if (this.moveTimer <= 0) {
this.waiting = false;
}
}
};
restStepTimer() {
this.moveTimer = Math.floor(Math.random() * 50 + 20);
};
};
Thanks for the help, code below now is functional.
Previous problem (resolved):
I'm creating a game, and the enemy loads, but doesn't move. The player moves, so I've been searching for the error specific to the enemy's movement, but I haven't been able to determine the fault.
Enemy gets called to move by this function.
function moveAll() {
redShip.move(); // moves player, working
enemy.move(); // moves enemy, works now after correction.
}
The Enemy's move function
const UFO_SPEED = 1.9;
const UFO_TIME_BETWEEN_CHANGE_DIR = 85;
UFOClass.prototype = new movingWrapPositionClass();
function UFOClass() {
this.x = Math.random()*600;
this.y = Math.random()*800;
this.xv = 0;
this.yv = 0;
this.cyclesTilDirectionChange = 0; // added line to make work
this.superclassMove = this.move;
this.move = function() {
this.superclassMove();
this.cyclesTilDirectionChange--;
if(this.cyclesTilDirectionChange <= 0) {
var randAng = Math.random() * Math.PI*2.0;
this.xv = Math.cos(randAng) * UFO_SPEED;
this.yv = Math.sin(randAng) * UFO_SPEED;
this.cyclesTilDirectionChange = UFO_TIME_BETWEEN_CHANGE_DIR;
}
}
Shared movement function between player and enemy
function movingWrapPositionClass() {
this.move = function() {
this.x += this.xv;
this.y += this.yv;
this.handleScreenWrap();
}
Adding the Ship's function which is working
const SPACESPEED_DECAY_MULT = 0.99;
const THRUST_POWER = 0.15;
const TURN_RATE = 0.06;
shipClass.prototype = new movingWrapPositionClass();
function shipClass() {
this.myShot = new shotClass();
this.x = 400;
this.y = 300;
this.ang = -3;
this.xv = 0;
this.yv = 0;
this.myShipPic; // which picture to use
this.keyHeld_Gas = false;
this.keyHeld_TurnLeft = false;
this.keyHeld_TurnRight = false;
this.controlKeyUp;
this.controlKeyRight;
this.controlKeyDown;
this.controlKeyLeft;
this.controlKeyForShotFire;
this.setupInput = function(upKey, rightKey, downKey, leftKey, shotKey) {
this.controlKeyUp = upKey;
this.controlKeyRight = rightKey;
this.controlKeyDown = downKey;
this.controlKeyLeft = leftKey;
this.controlKeyForShotFire = shotKey;
}
this.superclassMove = this.move;
this.move = function() {
this.x += Math.cos(this.ang) * this.xv;
this.y += Math.sin(this.ang) * this.yv;
this.superclassMove();
this.xv *= SPACESPEED_DECAY_MULT;
this.yv *= SPACESPEED_DECAY_MULT;
if(this.keyHeld_Gas) {
this.xv += THRUST_POWER;
this.yv += THRUST_POWER;
this.superclassMove();
}
if(this.keyHeld_TurnLeft) {
this.ang -= TURN_RATE;
}
if(this.keyHeld_TurnRight) {
this.ang += TURN_RATE;
}
Currently I am creating a Javascript game. I have successfully created all the game pieces and how my game's basic functions should work. However, I am having 2 issues that I can't seem to solve, and I've looked for a few weeks now trying to figure out how to do it. I can't figure out how to get the game to result in a gameover if my player gets pushed of falls off screen. Also, I am unsure on how I can get the score to continously increase for as long as the player is still "alive". If anyone can either demonstrate how to do these things or point me in the direction of a tutorial or article on how to do it it would be very helpful.
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
const seeded = (() => {
var seed = 1;
return {
max: 2576436549074795,
reseed(s) {
seed = s
},
random() {
return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
},
}
})();
const randSeed = (seed) => seeded.reseed(seed);
const randSI = (min, max = min + (min = 0)) => (seeded.random() % (max - min)) + min;
const randS = (min, max = min + (min = 0)) => (seeded.random() / seeded.max) * (max - min) + min;
randSeed(100); // seed the random generators
function Player(color, keymap, x) {
this.x = (typeof x === 'undefined') ? 1 : x;
this.y = 7;
this.width = 15;
this.height = 15;
this.speed = 10;
this.velX = 0;
this.velY = 0;
this.jumping = false;
this.keymap = {}
for (let key in keymap) {
switch (keymap[key]) {
case 'jump':
this.keymap[key] = this.jump
break;
case 'left':
this.keymap[key] = this.moveLeft
break;
case 'right':
this.keymap[key] = this.moveRight
break;
}
}
this.color = color;
} // Player()
Player.prototype.jump = function() {
if (!this.jumping) {
this.jumping = true;
this.velY = -this.speed * 1.5;
}
}
Player.prototype.moveRight = function() {
if (this.velX < this.speed) {
this.velX++;
}
}
Player.prototype.moveLeft = function() {
if (this.velX > -this.speed) {
this.velX--;
}
}
// Globals
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
width = 700,
height = 600,
keys = [],
friction = .9,
gravity = .9;
canvas.width = width;
canvas.height = height;
// Set up players
var players = [];
players.push(new Player('purple', {
32: 'jump',
37: 'left',
38: 'jump',
39: 'right'
}))
/*players.push(new Player('yellow', {
56: 'jump',
52: 'left',
54: 'right'
}, width-25))*/
players.push(new Player('blue', {
87: 'jump',
65: 'left',
68: 'right'
}, (width-25)/2))
function update() {
ctx.clearRect(0, 0, width, height);
addPlatformsToBottom(); // will add platforms if needed
drawPlatforms();
players.forEach(player => {
// check player-specific keys
for (let i in player.keymap) {
if (keys[i] && typeof player.keymap[i] === 'function')
player.keymap[i].bind(player)();
}
player.velX *= friction;
player.velY += gravity;
player.x += player.velX;
player.y += player.velY;
if (player.x >= width - player.width) {
player.x = width - player.width;
} else if (player.x <= 0) {
player.x = 0;
}
if (player.y >= height - player.height) {
player.y = height - player.height;
player.jumping = false;
player.velY = 0;
}
testPlayerForPlatforms(player);
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);
}) // player.forEach
requestAnimationFrame(update);
}
document.body.addEventListener("keydown", function(e) {
// console.log(e.keyCode);
keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function(e) {
keys[e.keyCode] = false;
});
window.addEventListener("load", function() {
update();
});
function testPlayerForPlatforms(player) {
player.hitPlatform = false; // reset platform hit flag
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (p.active) {
testPlayer(player, p);
if (player.hitPlatform) {
break; // stop search as player has hit a platform
}
}
}
}
function drawPlatforms() { // draws all platforms and move up
platformInfo.lastPlatformY += platformInfo.speed;
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (p.active) {
p.yPos += platformInfo.speed;
if (p.yPos + p.height < 0) { // platform above top
p.active = false; // turn it off
} else {
p.draw();
}
}
}
}
function addPlatformsToBottom() {
while (platformInfo.lastPlatformY < ctx.canvas.height) {
generateLevel();
}
}
// some constants and vars to control random generation of platforms
const platformInfo = {
speed: -2.5,
height: 8, // platform height
minLength: 100, // in pixels
maxLength: 300,
vertSpacing: 100, // distance between platforms
minHorSpacing: 50, // should be larger than player
maxHorSpacing: 80,
lastPlatformY: 100, // y position of last platform created
maxHoleCount: 3,
color: "#FFF",
}
// array object holds platforms
const platforms = [];
// a platform template object that will be used to create platforms from
const platform = {
left: 0,
right: 0,
yPos: 0,
height: 0, // thickness
active: false, // true if platform in use
color: "#F84",
draw() { // function to draw the platform
ctx.fillStyle = this.color;
ctx.fillRect(this.left, this.yPos, this.right - this.left, this.height);
},
init(left, right, yPos) { // function to initialize
// alias to save typing.
const pI = platformInfo
this.yPos = yPos;
this.left = left;
this.right = right;
this.height = pI.height;
this.color = pI.color;
this.active = true;
},
}
// function adds platforms to array. If no inactive platforms a
// new one is created
function addPlatform() {
var platform;
for (var i = 0; i < platforms.length; i++) {
if (!platforms[i].active) { // is the platform inactive
platform = platforms[i];
break; // stop searching
}
}
if (!platform) { // if no inactive platform then create a new one
platform = createPlatform();
platforms.push(platform);
}
return platform;
}
// a function to create a platform object
function createPlatform(customProps = {}) { // custom props can be used to modify the
// platform in future. For now it just defaults to empty
return Object.assign({}, platform, customProps);
}
// creates a set of platforms for a single level
function generateLevel() {
var numHoles = randSI(1, platformInfo.maxHoleCount);
var spacing = ctx.canvas.width / (numHoles); // get spacing
var ypos = platformInfo.lastPlatformY;
platformInfo.lastPlatformY += platformInfo.vertSpacing;
var left = 0; // the starting left edge
for (var i = 1; i <= numHoles; i++) { // create numHoles
var platform = addPlatform();
var holeOffset = randSI(-spacing, 0);
platform.init(left, spacing * i + holeOffset, ypos);
left = spacing * i + holeOffset + randSI(platformInfo.minHorSpacing, platformInfo.maxHorSpacing);
}
// add the last platform
platform = addPlatform();
platform.init(left, ctx.canvas.width, ypos);
}
function testPlayer(player, platform) {
var p, pl; // p for player, pl for platform
p = player;
pl = platform;
// is the player above or below platform
if (!(p.x + p.width < pl.left || p.x > pl.right)) { // yes
if (p.velY > 0 && p.y < pl.yPos) { // is player moving down and above platform
if (p.y + p.height > pl.yPos) { //is bottom of player below top of platform
// must have hit platform
p.jumping = false;
p.y = pl.yPos - p.height; // move player so that it is on the platform
p.velY = 0;
p.hitPlatform = true; // flag a platform has been hit
}
} else if (p.y + p.height > pl.yPos + pl.height) { // player must be moving up so check if below platform
if (p.y < pl.yPos + pl.height) { // is top of player above bottom of platform
// must have hit head on platform
p.velY = 0;
p.y = pl.yPos + pl.height;
p.jumping = false;
p.hitPlatform = true; // flag a platform has been hit
}
}
}
}
#score {
color:white;
font-size:35px;
}
0<html>
<head>
<title>Square Stairs™</title>
</head>
<body bgcolor="#000">
<div id="score">SCORE:</div>
<br><br><br> <!-- line breaks to move canvas away from SO title bar that gets in the way when switching to full page mode -->
<canvas id="canvas" style="border:3px solid #fff"></canvas>
</body>
</html>
This is my code as it stands currently. I made on the websiteCodePenand this is my original code. I have nothing to do with Unity or any other coding platform so if you have any information on how to solve this it has to be able to run on the CodePen website. Thank you for your assistance.
I've added code to your code, look for ////////////////////
I added the following to your source code:
Added the functions checkPlayerBounds(player) and addToScore(x)
checkPlayerBounds(player) checks if player has been pushed/fell off the screen.
Note: You need to add more functionality here to have the Game Over do what you want.
addToScore(x) increases the score variable and the Score HTML element if scoreShouldUpdate is true.
Added global variables updates, score, and scoreShouldUpdate.
updates keeps track of how many times the update() function has been called.
score keeps track of the games score.
Added code inside your update() function.
The first bit keeps adding to the score, but only after the game has been updated a specified amount of times.
The next bit calls the checkPlayerBounds function on every player.
This all works, the only problem is that the player starts off/right on the edge of the screen, so the score won't update until that is changed in your project.
(function() {
var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
window.requestAnimationFrame = requestAnimationFrame;
})();
const seeded = (() => {
var seed = 1;
return {
max: 2576436549074795,
reseed(s) {
seed = s
},
random() {
return seed = ((8765432352450986 * seed) + 8507698654323524) % this.max
},
}
})();
const randSeed = (seed) => seeded.reseed(seed);
const randSI = (min, max = min + (min = 0)) => (seeded.random() % (max - min)) + min;
const randS = (min, max = min + (min = 0)) => (seeded.random() / seeded.max) * (max - min) + min;
randSeed(100); // seed the random generators
///////////////////////////
// This function increases the score by 'x' amount.
///////////////////////////
function addToScore(x){
if(scoreShouldUpdate){
score += x;
var scoreElement = document.getElementById("score");
scoreElement.innerHTML = "Score: " + score;
}
}
function checkPlayerBounds(player){
if(player.y >= 0){
scoreShouldUpdate = false;
// Insert game over stuff here.
}
}
function Player(color, keymap, x) {
this.x = (typeof x === 'undefined') ? 1 : x;
this.y = 7;
this.width = 15;
this.height = 15;
this.speed = 10;
this.velX = 0;
this.velY = 0;
this.jumping = false;
this.keymap = {}
for (let key in keymap) {
switch (keymap[key]) {
case 'jump':
this.keymap[key] = this.jump
break;
case 'left':
this.keymap[key] = this.moveLeft
break;
case 'right':
this.keymap[key] = this.moveRight
break;
}
}
this.color = color;
} // Player()
Player.prototype.jump = function() {
if (!this.jumping) {
this.jumping = true;
this.velY = -this.speed * 1.5;
}
}
Player.prototype.moveRight = function() {
if (this.velX < this.speed) {
this.velX++;
}
}
Player.prototype.moveLeft = function() {
if (this.velX > -this.speed) {
this.velX--;
}
}
// Globals
var canvas = document.getElementById("canvas"),
ctx = canvas.getContext("2d"),
width = 700,
height = 600,
keys = [],
friction = .9,
gravity = .9;
//////////////////////////////
//New variables for score increase
//////////////////////////////
var updates = 0;
var score = 0;
var scoreShouldUpdate = true;
canvas.width = width;
canvas.height = height;
// Set up players
var players = [];
players.push(new Player('purple', {
32: 'jump',
37: 'left',
38: 'jump',
39: 'right'
}))
/*players.push(new Player('yellow', {
56: 'jump',
52: 'left',
54: 'right'
}, width-25))*/
players.push(new Player('blue', {
87: 'jump',
65: 'left',
68: 'right'
}, (width-25)/2))
function update() {
/////////////////////////////////////
// You can change the '5' to something else to change the rate of which score is increased.
/////////////////////////////////////
if(updates == 5){
updates = 0;
addToScore(1);
}
updates++;
ctx.clearRect(0, 0, width, height);
addPlatformsToBottom(); // will add platforms if needed
drawPlatforms();
players.forEach(player => {
// check player-specific keys
for (let i in player.keymap) {
if (keys[i] && typeof player.keymap[i] === 'function')
player.keymap[i].bind(player)();
}
player.velX *= friction;
player.velY += gravity;
player.x += player.velX;
player.y += player.velY;
if (player.x >= width - player.width) {
player.x = width - player.width;
} else if (player.x <= 0) {
player.x = 0;
}
if (player.y >= height - player.height) {
player.y = height - player.height;
player.jumping = false;
player.velY = 0;
}
testPlayerForPlatforms(player);
ctx.fillStyle = player.color;
ctx.fillRect(player.x, player.y, player.width, player.height);
//My code
checkPlayerBounds(player);
}) // player.forEach
requestAnimationFrame(update);
}
document.body.addEventListener("keydown", function(e) {
// console.log(e.keyCode);
keys[e.keyCode] = true;
});
document.body.addEventListener("keyup", function(e) {
keys[e.keyCode] = false;
});
window.addEventListener("load", function() {
update();
});
function testPlayerForPlatforms(player) {
player.hitPlatform = false; // reset platform hit flag
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (p.active) {
testPlayer(player, p);
if (player.hitPlatform) {
break; // stop search as player has hit a platform
}
}
}
}
function drawPlatforms() { // draws all platforms and move up
platformInfo.lastPlatformY += platformInfo.speed;
for (var i = 0; i < platforms.length; i++) {
var p = platforms[i];
if (p.active) {
p.yPos += platformInfo.speed;
if (p.yPos + p.height < 0) { // platform above top
p.active = false; // turn it off
} else {
p.draw();
}
}
}
}
function addPlatformsToBottom() {
while (platformInfo.lastPlatformY < ctx.canvas.height) {
generateLevel();
}
}
// some constants and vars to control random generation of platforms
const platformInfo = {
speed: -2.5,
height: 8, // platform height
minLength: 100, // in pixels
maxLength: 300,
vertSpacing: 100, // distance between platforms
minHorSpacing: 50, // should be larger than player
maxHorSpacing: 80,
lastPlatformY: 100, // y position of last platform created
maxHoleCount: 3,
color: "#FFF",
}
// array object holds platforms
const platforms = [];
// a platform template object that will be used to create platforms from
const platform = {
left: 0,
right: 0,
yPos: 0,
height: 0, // thickness
active: false, // true if platform in use
color: "#F84",
draw() { // function to draw the platform
ctx.fillStyle = this.color;
ctx.fillRect(this.left, this.yPos, this.right - this.left, this.height);
},
init(left, right, yPos) { // function to initialize
// alias to save typing.
const pI = platformInfo
this.yPos = yPos;
this.left = left;
this.right = right;
this.height = pI.height;
this.color = pI.color;
this.active = true;
},
}
// function adds platforms to array. If no inactive platforms a
// new one is created
function addPlatform() {
var platform;
for (var i = 0; i < platforms.length; i++) {
if (!platforms[i].active) { // is the platform inactive
platform = platforms[i];
break; // stop searching
}
}
if (!platform) { // if no inactive platform then create a new one
platform = createPlatform();
platforms.push(platform);
}
return platform;
}
// a function to create a platform object
function createPlatform(customProps = {}) { // custom props can be used to modify the
// platform in future. For now it just defaults to empty
return Object.assign({}, platform, customProps);
}
// creates a set of platforms for a single level
function generateLevel() {
var numHoles = randSI(1, platformInfo.maxHoleCount);
var spacing = ctx.canvas.width / (numHoles); // get spacing
var ypos = platformInfo.lastPlatformY;
platformInfo.lastPlatformY += platformInfo.vertSpacing;
var left = 0; // the starting left edge
for (var i = 1; i <= numHoles; i++) { // create numHoles
var platform = addPlatform();
var holeOffset = randSI(-spacing, 0);
platform.init(left, spacing * i + holeOffset, ypos);
left = spacing * i + holeOffset + randSI(platformInfo.minHorSpacing, platformInfo.maxHorSpacing);
}
// add the last platform
platform = addPlatform();
platform.init(left, ctx.canvas.width, ypos);
}
function testPlayer(player, platform) {
var p, pl; // p for player, pl for platform
p = player;
pl = platform;
// is the player above or below platform
if (!(p.x + p.width < pl.left || p.x > pl.right)) { // yes
if (p.velY > 0 && p.y < pl.yPos) { // is player moving down and above platform
if (p.y + p.height > pl.yPos) { //is bottom of player below top of platform
// must have hit platform
p.jumping = false;
p.y = pl.yPos - p.height; // move player so that it is on the platform
p.velY = 0;
p.hitPlatform = true; // flag a platform has been hit
}
} else if (p.y + p.height > pl.yPos + pl.height) { // player must be moving up so check if below platform
if (p.y < pl.yPos + pl.height) { // is top of player above bottom of platform
// must have hit head on platform
p.velY = 0;
p.y = pl.yPos + pl.height;
p.jumping = false;
p.hitPlatform = true; // flag a platform has been hit
}
}
}
}
#score {
color:white;
font-size:35px;
}
<html>
<head>
<title>Square Stairs™</title>
</head>
<body bgcolor="#000">
<div id="score">SCORE:</div>
<br><br><br> <!-- line breaks to move canvas away from SO title bar that gets in the way when switching to full page mode -->
<canvas id="canvas" style="border:3px solid #fff"></canvas>
</body>
</html>
I'm trying to update a barrage of bullets each frame in JavaScript, but it seems it's not working properly. Here's the code I put on JSFiddle.
https://jsfiddle.net/Reverseblade/co2mpLnv/5/
var ctx;
var hero;
var enemy;
var beams = [];
var beam_hitting = false;
var continuous_hit = false;
var count = 0;
var canvas_w = 500, canvas_y= 700;
var keycode = NaN;
var laser_degree = 200;
function init(){
createCanvas();
createMainHero();
createEnemy();
draw();
mainRoutine();
}
function mainRoutine(){
count++;
ctx.clearRect(0, 0, canvas_w, canvas_y);
// laserTest();
hero.move(keycode);
enemyUpdate();
// hero.setBullets();
// hero.moveBullets();
draw();
window.setTimeout(mainRoutine, 7);
}
function createCanvas(){
var canvas = document.createElement("canvas");
canvas.id = "canvas";
canvas.width = canvas_w;
canvas.height = canvas_y;
document.body.appendChild(canvas);
}
function createMainHero(){
hero = new Hero();
}
function createEnemy(){
enemy = new Enemy;
}
function Hero(){
this.w = this.h = 25,
this.x = canvas_w/2 - this.w/2,
this.y = canvas_y - this.h,
this.dx = this.dy = 2.5;
this.bullets = [];
this.move = function(key){
switch(key){
case 37: if (hero.x > 0) {hero.x -= this.dx;} break;
case 38: if (hero.y > 0) {hero.y -= this.dy;} break;
case 39: if (hero.x < canvas_w - hero.w) {hero.x += this.dx;} break;
case 40: if (hero.y < canvas_y - hero.h) {hero.y += this.dy;} break;
}
};
this.setBullets = function(){
if (count % 20 === 0) {
var w = h = 8;
var dx = dy = 5;
var x = hero.x + hero.w/2 - w/2;
var y = hero.y;
hero.bullets.push({x: x, y: y, w: w, h: h, dx: dx, dy: dy, moving:true});
}
};
this.moveBullets = function(){
for (var i = 0; i < this.bullets.length; i++) {
if (this.bullets[i].y < 0) {
this.bullets[i].moving = false;
continue;
}
if (this.bullets[i].moving === false) {
continue;
}
this.bullets[i].y -= this.bullets[i].dy;
if (this.bullets[i].y < -100) {this.bullets.splice(i, 1)}
}
}
}
function Enemy(){
this.w = this.h = 25;
this.x = canvas_w/2 - this.w/2;
this.y = 50;
this.bullets = [];
this.moving = false;
this.move_to = 0;
this.bullets_count = 0;
this.bullets_angle = 0;
this.current_skill = 1;
this.barrage_count = 0;
this.skill = function(){
enemySkill();
};
}
function enemyUpdate(){
function move(){
function changeDirection(){
var options = ["left", "right", "up", "down"];
var id;
if (enemy.x <= 50) {id = options.indexOf("left"); options.splice(id, 1);}
if (enemy.x >= 450) {id = options.indexOf("right");options.splice(id, 1);}
if (enemy.y <= 50) {id = options.indexOf("up");options.splice(id, 1);}
if (enemy.y >= 200) {id = options.indexOf("down");options.splice(id, 1);}
var rand = Math.floor(Math.random() * options.length);
enemy.moving = options[rand];
switch(enemy.moving){
case "left": enemy.move_to = enemy.x - 150 ; break;
case "right": enemy.move_to = enemy.x + 150 ; break;
case "up": enemy.move_to = enemy.y - 150 ; break;
case "down": enemy.move_to = enemy.y + 150 ; break;
}
} /* end changeDirection() */
if (count % 800 === 0) {changeDirection(); console.log("changing");}
switch(enemy.moving){
case "left": if (enemy.x > 50 && enemy.x > enemy.move_to) {enemy.x -= 0.5;} break;
case "right": if (enemy.x < 450 && enemy.x < enemy.move_to) {enemy.x += 0.5;} break;
case "up": if (enemy.y > 50 && enemy.y > enemy.move_to) {enemy.y -= 0.5; } break;
case "down": if (enemy.y < 200 && enemy.y < enemy.move_to) {enemy.y += 0.5; } break;
}
} /* end move()*/
move();
enemy.skill();
} /* end enemyUpdate() */
function enemySkill(){
// console.log("enemy skill");
function setBullets(){
var prev_status = enemy.bullets_count === 0 ? 500 : enemy.bullets[enemy.bullets.length - 1]["radius"];
if (prev_status >25) {
// console.log("bullets set");
var center_x = enemy.x + enemy.w/2;
var center_y = enemy.y + enemy.h/2;
var radius = 20;
var ceil = enemy.bullets.length === 0 ? 0 : enemy.bullets.length -1;
for (var angle = enemy.bullets_angle, i= ceil; angle < enemy.bullets_angle + 360; angle += 40, i++ ) {
// console.log("i: " + i);
var radian = angle * Math.PI / 180;
var set_x = center_x + radius * Math.cos(radian);
var set_y = center_y + radius * Math.sin(radian);
// console.log("angle: " + /angle + "set_x: " + set_x + "set_y: " + set_y);
enemy.bullets.push({"x": set_x, "y": set_y, "moving": true, "radius": radius, "center_x": center_x, "center_y": center_y, "w": 25, "h": 25, "radian": radian});
if (enemy.bullets_count === 0) {enemy.bullets_count++;}
// console.log(enemy.bullets[0][i]["x"]);
}
enemy.bullets_angle += 10;
enemy.barrage_count ++;
if (enemy.barrage_count % 100 === 0) {
enemy.current_skill = 0;
}
}
} /* end setBullets */
function moveBullets(){
if (count % 4 ===0) {
for (var i = 0; i < enemy.bullets.length; i++) {
if (enemy.bullets[i]["moving"] === true) {
var radian = enemy.bullets[i]["radian"];
var center_x = enemy.bullets[i]["center_x"];
var center_y = enemy.bullets[i]["center_y"];
enemy.bullets[i]["radius"] += 5;
var radius = enemy.bullets[i]["radius"];
var set_x = center_x + radius * Math.cos(radian);
var set_y = center_y + radius * Math.sin(radian);
// console.log(set_y);
enemy.bullets[i]["x"] = set_x;
enemy.bullets[i]["y"] = set_y;
if (enemy.bullets[i]["x"] < -100 || enemy.bullets[i]["x"] > canvas_w + 100 || enemy.bullets[i]["y"] < -100 || enemy.bullets[i]["y"] > canvas_y + 100 ) {
// enemy.bullets[i]["moving"] = false;
enemy.bullets.splice(i, 1);
}
}
}
}
}
if (enemy.current_skill === 1) {
setBullets();
}
moveBullets();
}
function draw(){
var canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
//hero
//ctx.fillStyle = "blue";
//ctx.fillRect(hero.x, hero.y ,hero.w, hero.h);
//enemy
//ctx.fillStyle = "red";
//ctx.fillRect(enemy.x, enemy.y ,enemy.w, enemy.h);
//heroの弾
ctx.fillStyle = "blue";
for (var i = 0; i < hero.bullets.length; i++) {
if (hero.bullets[i].moving === false) {
continue;
}
ctx.fillRect(hero.bullets[i].x, hero.bullets[i].y ,hero.bullets[i].w, hero.bullets[i].h);
}
//敵の弾
ctx.fillStyle = "red";
for (var i = 0; i < enemy.bullets.length; i++) {
ctx.fillStyle = "green";
if (enemy.bullets[i]["moving"] === false) {
continue;
}
ctx.beginPath();
ctx.arc(enemy.bullets[i]["x"], enemy.bullets[i]["y"], 15, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
}
}
window.addEventListener("keydown", function(e){
switch(e.keyCode){
case 37: keycode = 37; break;
case 38: keycode = 38; break;
case 39: keycode = 39; break;
case 40: keycode = 40; break;
}
}, false);
window.addEventListener("keyup", function(e){
switch(e.keyCode){
case 37: keycode = NaN; break;
case 38: keycode = NaN; break;
case 39: keycode = NaN; break;
case 40: keycode = NaN; break;
}
}, false);
init();
*{
margin:0;
padding:0;
}
#canvas{
background:url("../img/seamles_space.jpg");
animation: mainBack 5s linear infinite;
animation-play-state:paused;
background: black;
display: block;
position:relative;
margin:50px auto;
}
<body>
<script src="js/main.js"></script>
</body>
var ctx;
var hero;
var enemy;
var beams = [];
var beam_hitting = false;
var continuous_hit = false;
var count = 0;
var canvas_w = 500, canvas_y= 700;
var keycode = NaN;
var laser_degree = 200;
function init(){
createCanvas();
createMainHero();
createEnemy();
draw();
mainRoutine();
}
function mainRoutine(){
count++;
ctx.clearRect(0, 0, canvas_w, canvas_y);
// laserTest();
hero.move(keycode);
enemyUpdate();
// hero.setBullets();
// hero.moveBullets();
draw();
window.setTimeout(mainRoutine, 7);
}
function createCanvas(){
var canvas = document.createElement("canvas");
canvas.id = "canvas";
canvas.width = canvas_w;
canvas.height = canvas_y;
document.body.appendChild(canvas);
}
function createMainHero(){
hero = new Hero();
}
function createEnemy(){
enemy = new Enemy;
}
function Hero(){
this.w = this.h = 25,
this.x = canvas_w/2 - this.w/2,
this.y = canvas_y - this.h,
this.dx = this.dy = 2.5;
this.bullets = [];
this.move = function(key){
switch(key){
case 37: if (hero.x > 0) {hero.x -= this.dx;} break;
case 38: if (hero.y > 0) {hero.y -= this.dy;} break;
case 39: if (hero.x < canvas_w - hero.w) {hero.x += this.dx;} break;
case 40: if (hero.y < canvas_y - hero.h) {hero.y += this.dy;} break;
}
};
this.setBullets = function(){
if (count % 20 === 0) {
var w = h = 8;
var dx = dy = 5;
var x = hero.x + hero.w/2 - w/2;
var y = hero.y;
hero.bullets.push({x: x, y: y, w: w, h: h, dx: dx, dy: dy, moving:true});
}
};
this.moveBullets = function(){
for (var i = 0; i < this.bullets.length; i++) {
if (this.bullets[i].y < 0) {
this.bullets[i].moving = false;
continue;
}
if (this.bullets[i].moving === false) {
continue;
}
this.bullets[i].y -= this.bullets[i].dy;
if (this.bullets[i].y < -100) {this.bullets.splice(i, 1)}
}
}
}
function Enemy(){
this.w = this.h = 25;
this.x = canvas_w/2 - this.w/2;
this.y = 50;
this.bullets = [];
this.moving = false;
this.move_to = 0;
this.bullets_count = 0;
this.bullets_angle = 0;
this.current_skill = 1;
this.barrage_count = 0;
this.skill = function(){
enemySkill();
};
}
function enemyUpdate(){
function move(){
function changeDirection(){
var options = ["left", "right", "up", "down"];
var id;
if (enemy.x <= 50) {id = options.indexOf("left"); options.splice(id, 1);}
if (enemy.x >= 450) {id = options.indexOf("right");options.splice(id, 1);}
if (enemy.y <= 50) {id = options.indexOf("up");options.splice(id, 1);}
if (enemy.y >= 200) {id = options.indexOf("down");options.splice(id, 1);}
var rand = Math.floor(Math.random() * options.length);
enemy.moving = options[rand];
switch(enemy.moving){
case "left": enemy.move_to = enemy.x - 150 ; break;
case "right": enemy.move_to = enemy.x + 150 ; break;
case "up": enemy.move_to = enemy.y - 150 ; break;
case "down": enemy.move_to = enemy.y + 150 ; break;
}
} /* end changeDirection() */
if (count % 800 === 0) {changeDirection(); console.log("changing");}
switch(enemy.moving){
case "left": if (enemy.x > 50 && enemy.x > enemy.move_to) {enemy.x -= 0.5;} break;
case "right": if (enemy.x < 450 && enemy.x < enemy.move_to) {enemy.x += 0.5;} break;
case "up": if (enemy.y > 50 && enemy.y > enemy.move_to) {enemy.y -= 0.5; } break;
case "down": if (enemy.y < 200 && enemy.y < enemy.move_to) {enemy.y += 0.5; } break;
}
} /* end move()*/
move();
enemy.skill();
} /* end enemyUpdate() */
function enemySkill(){
// console.log("enemy skill");
function setBullets(){
var prev_status = enemy.bullets_count === 0 ? 500 : enemy.bullets[enemy.bullets.length - 1]["radius"];
if (prev_status >25) {
// console.log("bullets set");
var center_x = enemy.x + enemy.w/2;
var center_y = enemy.y + enemy.h/2;
var radius = 20;
var ceil = enemy.bullets.length === 0 ? 0 : enemy.bullets.length -1;
for (var angle = enemy.bullets_angle, i= ceil; angle < enemy.bullets_angle + 360; angle += 40, i++ ) {
// console.log("i: " + i);
var radian = angle * Math.PI / 180;
var set_x = center_x + radius * Math.cos(radian);
var set_y = center_y + radius * Math.sin(radian);
// console.log("angle: " + /angle + "set_x: " + set_x + "set_y: " + set_y);
enemy.bullets.push({"x": set_x, "y": set_y, "moving": true, "radius": radius, "center_x": center_x, "center_y": center_y, "w": 25, "h": 25, "radian": radian});
if (enemy.bullets_count === 0) {enemy.bullets_count++;}
// console.log(enemy.bullets[0][i]["x"]);
}
enemy.bullets_angle += 10;
enemy.barrage_count ++;
if (enemy.barrage_count % 100 === 0) {
enemy.current_skill = 0;
}
}
} /* end setBullets */
function moveBullets(){
if (count % 4 ===0) {
for (var i = 0; i < enemy.bullets.length; i++) {
if (enemy.bullets[i]["moving"] === true) {
var radian = enemy.bullets[i]["radian"];
var center_x = enemy.bullets[i]["center_x"];
var center_y = enemy.bullets[i]["center_y"];
enemy.bullets[i]["radius"] += 5;
var radius = enemy.bullets[i]["radius"];
var set_x = center_x + radius * Math.cos(radian);
var set_y = center_y + radius * Math.sin(radian);
// console.log(set_y);
enemy.bullets[i]["x"] = set_x;
enemy.bullets[i]["y"] = set_y;
if (enemy.bullets[i]["x"] < -100 || enemy.bullets[i]["x"] > canvas_w + 100 || enemy.bullets[i]["y"] < -100 || enemy.bullets[i]["y"] > canvas_y + 100 ) {
// enemy.bullets[i]["moving"] = false;
enemy.bullets.splice(i, 1);
}
}
}
}
}
if (enemy.current_skill === 1) {
setBullets();
}
moveBullets();
}
function draw(){
var canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
//hero
//ctx.fillStyle = "blue";
//ctx.fillRect(hero.x, hero.y ,hero.w, hero.h);
//enemy
//ctx.fillStyle = "red";
//ctx.fillRect(enemy.x, enemy.y ,enemy.w, enemy.h);
//heroの弾
ctx.fillStyle = "blue";
for (var i = 0; i < hero.bullets.length; i++) {
if (hero.bullets[i].moving === false) {
continue;
}
ctx.fillRect(hero.bullets[i].x, hero.bullets[i].y ,hero.bullets[i].w, hero.bullets[i].h);
}
//敵の弾
ctx.fillStyle = "red";
for (var i = 0; i < enemy.bullets.length; i++) {
ctx.fillStyle = "green";
if (enemy.bullets[i]["moving"] === false) {
continue;
}
ctx.beginPath();
ctx.arc(enemy.bullets[i]["x"], enemy.bullets[i]["y"], 15, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
}
}
window.addEventListener("keydown", function(e){
switch(e.keyCode){
case 37: keycode = 37; break;
case 38: keycode = 38; break;
case 39: keycode = 39; break;
case 40: keycode = 40; break;
}
}, false);
window.addEventListener("keyup", function(e){
switch(e.keyCode){
case 37: keycode = NaN; break;
case 38: keycode = NaN; break;
case 39: keycode = NaN; break;
case 40: keycode = NaN; break;
}
}, false);
init();
There's no problem at the beginning, but then some of the bullets would start acting weirdly as if after some point.
The cause is that an item is removed while in a for-loop, causing one bullet to be skipped, more precisely, this line:
enemy.bullets.splice(i, 1);
I would suggest a different approach - build a new array only consisting of the active bullets (moving===true), then after the loop replace the array with the new.
For example:
function moveBullets(){
if (count % 4 ===0) {
// will hold active bullets in current pass
var newBullets = [];
for (var i = 0; i < enemy.bullets.length; i++) {
// cut code for clarity
if (!(enemy.bullets[i].x < -100 || enemy.bullets[i].x > canvas_w + 100 ||
enemy.bullets[i].y < -100 || enemy.bullets[i].y > canvas_y + 100 )) {
newBullets.push(enemy.bullets[i]);
}
}
// replace array with only active bullets
enemy.bullets = newBullets;
}
}
The new array will simply hold references to existing active bullets.
Modified Fiddle
Particles and object pools.
Creating a new array is not a good strategy when dealing with particle systems. Which is what bullets are in effect.
Each bullet deleted, will need to be cleaned up by GC (Garbage Collection), each bullet added needs to be created and requires memory allocation. These overhead can have a negative effect on a game. GC can cause an animation to hang at random times.
For consistent, smooth animation you should aim for zero allocation and deletion (which is possible as asm.js and web Assembly do not allocate or delete inside a running modula).
Object pool
In vanilla JS it is possible but the code is too complex for this answer. The next best solution is to use a object pool.
As bullets are first created as normal, but when a bullet is no longer needed rather than dereference it you move it to another array (called the pool), next time a bullet is needed you first check if any are available on the pool and use that rather than create a new object.
This ensures that GC only has to clean up the changing array sizes and not the data used by each bullet.
var bullets = [];
var bulletPool = [];
function createBullet(x,y,dir,whatNot){
var newBullet;
if(bulletPool.length > 0){
newBullet = bulletPool.pop(); // reuse old bullet memory
newBullet.x = x;
newBullet.y = y;
newBullet.dir = dir;
newBullet.whatNot = whatNot;
newBullet.active = true;
}else{
newBullet = {x,y,dir,whatNot,active:true}; // create only if needed
}
return newBullet;
}
function fire(){
bullets.push(createBullet(10,10,0,0)); /// add a bullet
}
In your code when a bullet is no longer needed just set the active flag to false. At the end of the game loop remove all the inactive bullets.
When you delete a bullet just move it from the bullet array to the pool
function cleanBulletsArray(){
for(var i = 0; i < bullets.length; i ++){
if(!bullets[i].active){
bulletPool.push(bullets.splice(i--,1)[0]);
}
}
}
Precipitation array
So called because the active items fall to the bottom of the array
An even better method is to use just one array. When a bullet is not active it stays in the array, but as you iterate the array you swap active bullets for inactive, with active bullets moving down and inactive up. There is at most one swap per iteration. You also keep a count of the number of active bullets. When you need a new bullet you just reset the properties of the bullet at index count + 1 and then increase count by one.
This method (if you pre allocate all the bullets at the start of the game) has zero GC and Memory overhead and is significantly quicker than the creation, destruction, and array replacement methods.