I have created this program that checks the collisions of balls as they move in random directions. Each ball has life points, that decrease with each collision. I want my program to print out a message "character has died" when a ball is considered dead. The problem is that I don't know how to replace a dead ball with a new one in my balls
array, as I don't know at which index to perform the replacement.
let balls = [];
let numBalls = 20;
let ballTextures = [];
let subaruImg, emiliaImg, remImg;
//------- Variables initialsed above ---------- //
function preload(){
subaruImg = loadImage('assets/subaru_chibi.jpg');
emiliaImg = loadImage('assets/emilaia_chibi.jpg');
remImg = loadImage('assets/rem_chibi.jpg');
ballTextures = [subaruImg, emiliaImg, remImg]; // Store my GOAT subaru and his friends in the array
}
// Preload is for images and textures
function setup() {
createCanvas(600, 600, WEBGL);
angleMode(DEGREES);
for (let i = 0; i < numBalls; i++) {
let pos = createVector(
random(-width / 2 + 20, width / 2 - 20),
random(-height / 2 + 20, height / 2 - 20),
random(-200, 200)
);
let vel = p5.Vector.random3D().mult(random(1, 2));
let r = random(20, 30);
// Let
let tex = random(ballTextures); //allows the t
balls.push(new Ball(pos, vel, r, tex));
}
frameRate(60);
}
function draw() {
background(20);
orbitControl(); // makes me be able to have 3d movementt on the canvas
//different lights
ambientLight(60, 60, 60);
directionalLight(255, 255, 255, -0.5, -1, -0.5);
// Moving point light (dynamic, cool effect)
let lightX = sin(frameCount * 0.01) * 300;
let lightY = cos(frameCount * 0.01) * 300;
let lightZ = sin(frameCount * 0.005) * 300;
pointLight(255, 255, 200, lightX, lightY, lightZ);
// Optional: visualize the light source
push();
translate(lightX, lightY, lightZ);
noStroke();
emissiveMaterial(255, 255, 200);
sphere(5); // glowing light orb
pop();
for (let i = 0; i < balls.length; i++) {
balls[i].move();
balls[i].edgeCheck();
balls[i].drawBall();
for (let j = i + 1; j < balls.length; j++) {
balls[i].checkCollision(balls[j]);
}
}
}
class Ball {
constructor(pos, vel, r, tex) {
this.pos = pos;
this.vel = vel;
this.r = r;
this.tex = tex;
this.life = 3; // start with 3 lives
this.alive = true;
}
drawBall() {
if (!this.alive) return; // don't draw if dead
push();
translate(this.pos.x, this.pos.y, this.pos.z);
texture(this.tex);
noStroke();
sphere(this.r);
pop();
}
move() {
if (!this.alive) return;
this.pos.add(this.vel);
}
edgeCheck() {
if (!this.alive) return;
if (this.pos.x + this.r > width / 2 || this.pos.x - this.r < -width / 2) {
this.vel.x *= -1;
}
if (this.pos.y + this.r > height / 2 || this.pos.y - this.r < -height / 2) {
this.vel.y *= -1;
}
if (this.pos.z + this.r > 300 || this.pos.z - this.r < -300) {
this.vel.z *= -1;
}
}
checkCollision(other) {
if (!this.alive || !other.alive) return;
let d = p5.Vector.dist(this.pos, other.pos);
if (d < this.r + other.r) {
// Swap velocities
let temp = this.vel.copy();
this.vel = other.vel.copy();
other.vel = temp;
// Separate overlapping balls
let overlap = (this.r + other.r - d) / 2;
let direction = p5.Vector.sub(this.pos, other.pos).normalize();
this.pos.add(direction.mult(overlap));
other.pos.sub(direction.mult(overlap));
// Decrease life
this.lifeCounter();
other.lifeCounter();
}
}
lifeCounter() {
this.life -= 1;
if (this.life <= 0) {
this.alive = false && new Ball(pos, vel, r, tex.subaruImg);
console.log("subaru has died");
}
if(this.life <=0) {
this.alive = false && new Ball(pos, vel, r, tex.emiliaImg);
console.log("emilia has died")
}
if(this.life <= 0){
this.alive = false && new Ball(pos, vel, r, tex.remImg);
console.log("Rem has died")
}
}
}
html, body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src=".js/1.11.1/p5.js"></script>
<script src=".js/1.11.1/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<main>
</main>
<script src="sketch.js"></script>
</body>
</html>
I have created this program that checks the collisions of balls as they move in random directions. Each ball has life points, that decrease with each collision. I want my program to print out a message "character has died" when a ball is considered dead. The problem is that I don't know how to replace a dead ball with a new one in my balls
array, as I don't know at which index to perform the replacement.
let balls = [];
let numBalls = 20;
let ballTextures = [];
let subaruImg, emiliaImg, remImg;
//------- Variables initialsed above ---------- //
function preload(){
subaruImg = loadImage('assets/subaru_chibi.jpg');
emiliaImg = loadImage('assets/emilaia_chibi.jpg');
remImg = loadImage('assets/rem_chibi.jpg');
ballTextures = [subaruImg, emiliaImg, remImg]; // Store my GOAT subaru and his friends in the array
}
// Preload is for images and textures
function setup() {
createCanvas(600, 600, WEBGL);
angleMode(DEGREES);
for (let i = 0; i < numBalls; i++) {
let pos = createVector(
random(-width / 2 + 20, width / 2 - 20),
random(-height / 2 + 20, height / 2 - 20),
random(-200, 200)
);
let vel = p5.Vector.random3D().mult(random(1, 2));
let r = random(20, 30);
// Let
let tex = random(ballTextures); //allows the t
balls.push(new Ball(pos, vel, r, tex));
}
frameRate(60);
}
function draw() {
background(20);
orbitControl(); // makes me be able to have 3d movementt on the canvas
//different lights
ambientLight(60, 60, 60);
directionalLight(255, 255, 255, -0.5, -1, -0.5);
// Moving point light (dynamic, cool effect)
let lightX = sin(frameCount * 0.01) * 300;
let lightY = cos(frameCount * 0.01) * 300;
let lightZ = sin(frameCount * 0.005) * 300;
pointLight(255, 255, 200, lightX, lightY, lightZ);
// Optional: visualize the light source
push();
translate(lightX, lightY, lightZ);
noStroke();
emissiveMaterial(255, 255, 200);
sphere(5); // glowing light orb
pop();
for (let i = 0; i < balls.length; i++) {
balls[i].move();
balls[i].edgeCheck();
balls[i].drawBall();
for (let j = i + 1; j < balls.length; j++) {
balls[i].checkCollision(balls[j]);
}
}
}
class Ball {
constructor(pos, vel, r, tex) {
this.pos = pos;
this.vel = vel;
this.r = r;
this.tex = tex;
this.life = 3; // start with 3 lives
this.alive = true;
}
drawBall() {
if (!this.alive) return; // don't draw if dead
push();
translate(this.pos.x, this.pos.y, this.pos.z);
texture(this.tex);
noStroke();
sphere(this.r);
pop();
}
move() {
if (!this.alive) return;
this.pos.add(this.vel);
}
edgeCheck() {
if (!this.alive) return;
if (this.pos.x + this.r > width / 2 || this.pos.x - this.r < -width / 2) {
this.vel.x *= -1;
}
if (this.pos.y + this.r > height / 2 || this.pos.y - this.r < -height / 2) {
this.vel.y *= -1;
}
if (this.pos.z + this.r > 300 || this.pos.z - this.r < -300) {
this.vel.z *= -1;
}
}
checkCollision(other) {
if (!this.alive || !other.alive) return;
let d = p5.Vector.dist(this.pos, other.pos);
if (d < this.r + other.r) {
// Swap velocities
let temp = this.vel.copy();
this.vel = other.vel.copy();
other.vel = temp;
// Separate overlapping balls
let overlap = (this.r + other.r - d) / 2;
let direction = p5.Vector.sub(this.pos, other.pos).normalize();
this.pos.add(direction.mult(overlap));
other.pos.sub(direction.mult(overlap));
// Decrease life
this.lifeCounter();
other.lifeCounter();
}
}
lifeCounter() {
this.life -= 1;
if (this.life <= 0) {
this.alive = false && new Ball(pos, vel, r, tex.subaruImg);
console.log("subaru has died");
}
if(this.life <=0) {
this.alive = false && new Ball(pos, vel, r, tex.emiliaImg);
console.log("emilia has died")
}
if(this.life <= 0){
this.alive = false && new Ball(pos, vel, r, tex.remImg);
console.log("Rem has died")
}
}
}
html, body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare/ajax/libs/p5.js/1.11.1/p5.js"></script>
<script src="https://cdnjs.cloudflare/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<main>
</main>
<script src="sketch.js"></script>
</body>
</html>
Here is a screenshot of when the balls start moving across the screen:
However when a ball "dies", then in the console log it shows three messages -- one for each name, so always in groups of three:
The section where this happens is in the lifeCounter
method in the Ball
class at the end of the code:
lifeCounter() {
this.life -= 1;
if (this.life <= 0) {
this.alive = false && new Ball(pos, vel, r, tex.subaruImg);
console.log("subaru has died");
}
if(this.life <=0) {
this.alive = false && new Ball(pos, vel, r, tex.emiliaImg);
console.log("emilia has died")
}
if(this.life <= 0){
this.alive = false && new Ball(pos, vel, r, tex.remImg);
console.log("Rem has died")
}
}
Instead of three messages per ball "death", I want to print one message only, and have the ball replaced with a new Ball
instance, but I have no idea how I can know where to put it in the array balls
, so currently the array is not updated and keeps continuing with balls that already "died". How can I achieve this update?
1 Answer
Reset to default 1There are several issues:
When
this.life <= 0
is true, it will be true in the three copies you have of the sameif
statement, and so you'll always print three names each time it is true.tex
does not have a property that is calledsubaruImg
(noremiliaImg
orremImg
). You don't actually know the name of the ball that died. To know the name, you would better store this as a ball property when the ball is created.&&
is a short-circuit operator, so if the left operand is false, the right operand will not be evaluated. That means that in your&&
expressions thenew Ball
expression will never be evaluated: no new ball is created.pos
,vel
andr
are not variables that are in scope, so executing thosenew Ball
expressions would not lead to desired behaviour anyway.
As to your main question: indeed you don't have the index where in the balls
array the current ball is placed. But this is not something that is the responsibility of your Balls
class. If you want to replace a ball in the balls
array, this should happen outside of that class, like in the loop where you call the checkCollision
method.
I have updated your snippet with the following changes:
The textures are now stored in a plain object that is keyed by the (avatar) names. This way you have the correspondence between a name and a texture. This affects the code of
preload
and of the random selection of textures.The
Ball
constructor takes an extra parameter for the name that is associated with the ball, and that name is stored as a new instance property.The erroneous code in
lifeCounter
has been removed, and so it just updates the counter and thealive
boolean -- that's all it needs to do.The death-check and replacement of a dead ball by a new one now happens in the loop that calls
checkCollision
.
As this snippet has no access to your images, I have renamed preload
so it doesn't execute here -- you would of course enable it in your own environment. I have removed all the comments in your code -- not because they are bad, but to highlight with my own comments where I have changed the code:
let balls = [];
let numBalls = 20;
// Make the textures collection a plain object, keyed by (avatar) name
const ballTextures = {
"subaru": null, // null will be replaced by image in preload()
"emilaia": null,
"rem": null,
};
//------- Variables initialsed above ---------- //
function _preload(){ // de-activated for this demo (by adding underscore)
// Load the textures based on the names we defined
for (const name in ballTextures) {
ballTextures[name] = loadImage(`assets/${name}_chibi.jpg`);
}
}
function setup() {
createCanvas(600, 600, WEBGL);
angleMode(DEGREES);
for (let i = 0; i < numBalls; i++) {
balls.push(randomBall()); // Moved ball generation code into separate function
}
frameRate(60);
}
function randomBall() { // New function
let pos = createVector(
random(-width / 2 + 20, width / 2 - 20),
random(-height / 2 + 20, height / 2 - 20),
random(-200, 200)
);
let vel = p5.Vector.random3D().mult(random(1, 2));
let r = random(20, 30);
// Choose a random name instead of a texture;
// we can get the texture from the name later:
const name = random(Object.keys(ballTextures));
// Add the name as an argument for the Ball constructor:
return new Ball(pos, vel, r, ballTextures[name], name);
}
function draw() {
background(20);
orbitControl();
ambientLight(60, 60, 60);
directionalLight(255, 255, 255, -0.5, -1, -0.5);
let lightX = sin(frameCount * 0.01) * 300;
let lightY = cos(frameCount * 0.01) * 300;
let lightZ = sin(frameCount * 0.005) * 300;
pointLight(255, 255, 200, lightX, lightY, lightZ);
push();
translate(lightX, lightY, lightZ);
noStroke();
emissiveMaterial(255, 255, 200);
sphere(5);
pop();
for (let i = 0; i < balls.length; i++) {
balls[i].move();
balls[i].edgeCheck();
balls[i].drawBall();
for (let j = i + 1; j < balls.length; j++) {
balls[i].checkCollision(balls[j]);
// Perform ball replacement here instead of in Ball class
if (!balls[i].alive) {
// Retrieve the name and report its death:
console.log(balls[i].name, "has died");
// Replace with a new ball: here we have access to array index:
balls[i] = randomBall(); // Create the ball as you please...
console.log(balls[i].name, "was born");
}
}
}
}
class Ball {
constructor(pos, vel, r, tex, name) { // Added name as parameter
this.pos = pos;
this.vel = vel;
this.r = r;
this.tex = tex;
this.name = name; // Added name property
this.life = 3;
this.alive = true;
}
drawBall() {
if (!this.alive) return;
push();
translate(this.pos.x, this.pos.y, this.pos.z);
//texture(this.tex); // skip texture for this demo only
noStroke();
sphere(this.r);
pop();
}
move() {
if (!this.alive) return;
this.pos.add(this.vel);
}
edgeCheck() {
if (!this.alive) return;
if (this.pos.x + this.r > width / 2 || this.pos.x - this.r < -width / 2) {
this.vel.x *= -1;
}
if (this.pos.y + this.r > height / 2 || this.pos.y - this.r < -height / 2) {
this.vel.y *= -1;
}
if (this.pos.z + this.r > 300 || this.pos.z - this.r < -300) {
this.vel.z *= -1;
}
}
checkCollision(other) {
if (!this.alive || !other.alive) return;
let d = p5.Vector.dist(this.pos, other.pos);
if (d < this.r + other.r) {
let temp = this.vel.copy();
this.vel = other.vel.copy();
other.vel = temp;
let overlap = (this.r + other.r - d) / 2;
let direction = p5.Vector.sub(this.pos, other.pos).normalize();
this.pos.add(direction.mult(overlap));
other.pos.sub(direction.mult(overlap));
this.lifeCounter();
other.lifeCounter();
}
}
lifeCounter() {
this.life -= 1;
this.alive = this.life > 0; // One assignment only (removed rest of code)
}
}
html, body {
margin: 0;
padding: 0;
}
canvas {
display: block;
}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare/ajax/libs/p5.js/1.11.1/p5.js"></script>
<script src="https://cdnjs.cloudflare/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<main>
</main>
</body>
</html>
I didn't touch, nor verify, other functionality in your code, but this change should answer your question.
this.alive = false && new Ball(pos, vel, r, tex.remImg);
can be simplythis.alive = false
. I suggest a runnable, minimal example--remove textures, lighting and things that don't pertain to the question. Thanks. – ggorlen Commented Mar 27 at 4:37