-
Notifications
You must be signed in to change notification settings - Fork 20
Pop Dots Game
Let's dive right in, shall we?
First let's start with a simple Rectangle:
Make it move when you move the Mouse around:
Now we'll get rid of the smeary trails by refreshing the Background before each redraw.
We'll change our Paddle Width and constrain it's Y Position to the lower part of the Screen:
Observe how the Paddle is Positioned so it's Left side follows the Mouse. Let's make it so the Mouse Position is adjusted to the Paddle's Center:
If we're going to have things bouncing off the top of our Paddle later on, we're going to want to know where the Left, Top and Right sides of our Paddle are. Just to plan ahead a bit, we will go ahead and convert the absolute (numerical) values in our Rectangle into named Variables. This will make the code more readable as well as make it easier to "tune" our program later on. For instance, if we want to change our Paddle Height or Y Position later, we don't have to search through the code to find it. All of our Game's Parameters are easily accessible at the top of our code :)
OK, now that we've got our Paddle out of the way, let's put that aside for a moment and work on some Ball logistics, starting from Dot:
and of course same old schtick, let's attach some Variables to our Ball:
Let's add some Motion to our Ball:
Instead of updating the Motion using the Absolute value (1), we assign a Variable at the top for tweakability:
Now our Ball goes right off the Screen. Let's make it Reverse Direction when it reaches the edge of the screen:
OK Great! Now our ball goes half way off screen before reversing direction. Fortunately we can use our BALL_SIZE Variable to make adjustments. We'll just subtract half the Ball Size.
Also our screen is 400 Pixels Wide, however we start counting at 0 instead of 1, so technically that number should be 399. You could also just use width
, which is a preset Variable that will tell you the screen width. This is useful if you are making JavaScript programs which can be scaled to the size of your browser. Since the Khan Program Window defaults to 400, we'll just pencil in the Absolute of 399.
Let's have it Bounce off the other wall as well. The left side of the screen is at 0, however that would take us halfway into the wall on the left, so we'll say the left wall limit is half the Ball size again:
Now we'll do the same for the Y axis of the Ball:
Our Draw Loop is getting a bit cluttered with all this code relating to updating our Ball now. To keep things nice and readable in our Draw Loop, we will copy all the code out of the Draw Loop and put it all inside a Function which is named after what it does:
Although the part where we Draw the Ball is only one line of code, we'll go ahead and do the same thing to the ellipse, just for consistency. Observe how the inside of your Draw Loop is now free of all loose code and reads like a sentence:
While we're on a roll converting code into Functions, let's pop on back to our Paddle program and do the same to it as well:
OK, now we have two pieces of our puzzle. Let's bring them together. Instead of just making a Spin-Off, we will start by creating a New Program. This will give us a clean slate to work with and wipe any unused Variables we may have inadvertently created which still reside in our Program's memory.
Open a New Tab in your Web Browser and find our Ball Program. First we'll copy the Bouncy Ball over to our New Program. Now locate our Paddle Program. We will start with just the Variables. Lets copy them over and check that we haven't accidentally named any Variables the same thing from the 2 different files.
Now we will copy over our Paddle Functions and paste them outside the Draw Loop and Copy & Paste our Function calls inside the Draw Loop:
OK, let's Save our New Program and call it "myProgramName Beta" or whatever. When we do, the Khan editor will add a whole bunch of extra white space in our program. This is because we copied our code from another window. We get a nice clean program with no hidden unwanted variables, but the drawback is: we have to go back and delete all the extra white spaces. This is a good time to add any comments to our work and organize it with proper indentation, etc. which will help keep it more readable later on as we go. Now hit Save one more time and we should be on our way.
Our next step will be to get our Ball to bounce off our Paddle. First you want to understand basic Collision Detection. Contemplate this Program for a moment:
OK, now that you got that, we're only going to need the Top and Sides. This is where our paddleLeft, PADDLE_TOP, and PADDLE_WIDTH variables that we made earlier come in handy:
We want to make it so if the Player misses the ball, they lose a play. Let's say the player has 3 Lives. We'll make a new Variable at the top for the number of plays, and then all the way at the bottom in our Draw Loop, we'll print the Variable to the screen using Text. We won't display it in our game later. This is for "debugging" purposes:
OK, now that we have our Variable where we can see it, let's make it so instead of bouncing off the floor like before, the Ball can now go right offscreen. If it does, it will subtract 1 from the Variable and we should see the results. Also we will set it right back to the top of the screen again for the next play:
After a bit of testing we can see that if the Ball is underneath the Paddle, it will still rebound off the floor. We have to further refine the area which will affect our collision to the bottom of the Paddle as well:
This is not 100% perfect collision between a square and a round object, but for learning purposes it will suit us OK for now.
Next we will make it so when the Number of Lives = 0, it will advance the Program to a Game Over Screen. Here is a One-Up example of how we will accomplish that. Please give this one a moment of your scrutiny:
Make your own Spin-Off or recreate your own Switch / Case example from scratch (recommended).
Now like before we will copy over all of our Variables first. Then we will copy the entire Switch Statement along with all of the Case Statements inside, and Paste it right after the background()
color in the Draw Loop:
Now it's pretty straightforward. You should now have your running game code right beneath your Switch & Case Statements in the Draw Loop. We will simply Cut the Ball & Paddle Functions along with the Text line that prints the playerLives
to the Screen and scroll up to the case "Main":
Segment and Paste it right where it says text("Game Code Goes Here\n\nPr...etc.
Then we will change the part that says:
if(keyCode === 49){ // If the 1 Key is pressed...
gameState = 1;// Now you know the rules. Begin Level 1
}
to this:
if(playerLives === 0){
gameState = false; // This will default to the Game Over Screen
}
Now after you let it miss 3 times, it should default to the Game Over segment of your Switch / default: Case Statement:
There is a glitch if you try to Start Over. That is because the Number of Player Lives is still 0. Therefore it will keep defaulting back to the end. All we need to do is reinitialize the Variables again for the next round here:
if(keyCode === LEFT){
gameState = "Main";
}
by reinitializing the Player Lives:
if(keyCode === LEFT){
playerLives = 3;
gameState = "Main";
}
Great! Now it goes back to the Main Screen. Only now we have another glitch. After replay, when we get to the Game Over Screen, it immediately jumps right back into play mode. To figure out what's up, we need to do a bit more debugging. Since the Game Over screen jumping to the Replay is triggered by the keyCode Variable, something must be wrong there, so let's print it to the screen by Copying and Pasting the bit of text that prints the Player Lives to the screen, offset the Y a bit and use that to print out our keyCode Variable and see what's up:
Game's Not Over till It's Over
Ah hah! What we see is that the value of the keyCode remains in effect as we are going into the Main level and still holding down the Arrow Key. We could do a hack fix for just that. Or to remedy any future problems from other keys, we can simply go all the way to the bottom of the Draw Loop and after all is said and done, reinitialize the keyCode to 0.
Normally in a Breakout Game you would be Breaking Blocks. Just for variety, since we have already learned basic Rectangular Collision Detection, we will try to make it so you destroy circles. To get the distance between two points is pretty simple using dist(point1X, point1Y, point2X, point2Y)
. That will tell us the distance between the Center of each Circle, to determine if the edges are touching, we simply need to add half the width of the 2 Circles. Relect on this for a brief interlude:
Allright, let's start working on some enemies to destroy. Lot's of em! Let's use our Ball Program from earlier, make it a little bit bigger, and move it up to the approximate area where our enemies will lurk:
We will declare an empty Array to store things to shoot at, like this: var skeets = [];
Then we will "push" a JSON Object onto the Array, which can store more than one Variable. Also known as an Enumerator. In this case we will store the X & Y Position of our Enemy:
skeets.push({ // Add entry to Array
x: 200,
y: 100
});
We know that the first Array entry is at 0. We can access the X & Y Values stored in the Array and use them to position our ellipse:
ellipse(skeets[0].x, skeets[0].y, 30, 30);
Instead of Absolute X & Y Numerical Values, let's assign some random numbers for now. Keeping them within the area around the top half of the screen:
skeets.push({ // Add entry to Array
x: random(10,390),
y: random(10,200)
});
Now when you hit the Restart Button, the Skeet should initialize to a different Random Position each time.
We'll also add some Random Color values from 0 - 255:
skeets.push({ // Add entry to Array
x: random(10,390),
y: random(10,200),
c: color(random(255), random(255), random(255))
});
Then we will access the color value stored in the Array and apply it to the fill() color:
draw = function(){ // Begin Draw Loop
background(0); // Clear the screen Black
fill(skeets[0].color);
ellipse(skeets[0].x, skeets[0].y, 30, 30);
}; // End Draw Loop
Now when you Restart the Program the Skeet should have Random Colors ☺
To add multiple Skeets to our Array, we'll use a For Loop
var numObjects = 4; // Number of Skeets to add
// Using a For Loop to populate the Array
for(var i = 0; i < numObjects; i++) {
// Push a new Array Entry
skeets.push({ // Add entry to Array
x: random(10, 390),
y: random(10, 390),
c: color(random(255), random(255), random(255))
});
}
Now we have 4 Objects stored in our Array, but we are still only printing one to the screen. We will use another For Loop to cycle through the Array and Draw each to the Screen:
for(var ob = 0; ob < skeets.length; ob++) {
fill(skeets[ob].color);
// Draw Circles at coordinates in Array
ellipse(skeets[ob].x, skeets[ob].y, 30, 30);
}
Observe that I use different Variable names in the next For Loop to avoid conflicts. Once the For Loop Variable has been created it is still accessible as a Public Variable. Which could also be used to intentionally affect the parameters of a second For Loop for various purposes.
Now let's move all the code related to Drawing our Objects into a Function. While we're at it, let's change the Size of our Skeet Sprite from the penciled in Absolute Value of 30. First we'll add a new Variable at the top:
var skeetSize = 30;
then down in the drawSkeets Function, we will replace the Numerical Values:
ellipse(skeets[ob].x, skeets[ob].y, skeetSize, skeetSize);
Looks like enemies are amassing all around! Better devote a little R & D toward a secret weapon to annihilate our foes. We will use another For Loop to infiltrate their hideout and eliminate the varmints using splice(arrayNumber, Quantity)
. For testing purposes, we will check for Mouse Collision using dist()
as before. The skeetSize Variable we made earlier comes in handy for that:
This is a down & dirty quickie way to get it working for testing purposes, but can lead to problems in gameplay resulting from Objects that are trying to be interacted with or Drawn after they have already been deleted from the Array. Very messy problem to deal with. You can play with it a bit and you may see some unexpected effects or get an error message. Basically, instead of having a Sprite which is trying to delete itself, one solution is to have a separate Garbage Collector Function. After all other Functions have had their moment of fame, Agent Smith comes and does the clean up work.
In this case, instead of deleting the Sprite directly, we will put out a Hit List on it, and let the Terminator come in later and do the dirty work.
To begin, we will add a new Parameter to the JSON Object in the Array for Collision:
// Using a For Loop to populate the Array
for(var i = 0; i < numObjects; i++) {
// Push a new Array Entry
skeets.push({ // Add entry to Array
x: random(10, 390),
y: random(10, 200),
color: color(random(255), random(255), random(255)),
collision: false // New entry for Collision
});
}
Now in the Update Function, if a Collision is detected, we will mark it in the Sprite's Parameters
var updateSkeets = function(){
for(var obj = 0; obj < skeets.length; obj++) {
// Check distance from center and Mark Array entry
if( dist(skeets[obj].x, skeets[obj].y, mouseX, mouseY) < skeetSize / 2 ) {
skeets[obj].collision = true; // Mark it for Termination
}
}
};
OK, the offending enemy has been marked for Termination. Before we call in the Bladerunner, let's clean house a bit. We'll take all of the code we have made for updating our Skeet and put it into a Function:
Now we'll create one more For Loop which will cycle through the Skeets List and Splice any Array entries that have been marked for Termination:
Agent Smith (Garbage Colllection)
and lastly, we will put our Terminator code into it's own Function:
There is one more stray bit of code which is currently only being loaded once at startup, but will most certainly be needed again later if we want more levels: the part where we populate our Array with Skeets:
// Using a For Loop to populate the Array
for(var i = 0; i < numObjects; i++) {
// Push a new Array Entry
skeets.push({ // Add entry to Array
x: random(10, 390),
y: random(10, 200),
color: color(random(255), random(255), random(255)),
collision: false
});
}
So let's wrap that all up into a nice Function. Then right underneath, in this example, we will go ahead and call the Function one time:
var createSkeets = function(){
// Using a For Loop to populate the Array
for(var i = 0; i < numObjects; i++) {
// Push a new Array Entry
skeets.push({ // Add entry to Array
x: random(10, 390),
y: random(10, 200),
color: color(random(255), random(255), random(255)),
collision: false
});
}
};
createSkeets(); // Call the createSkeets Function one time at startup
Hit the reload Button and hopefully you should have a functioning module :)
OK, now that we have another Game Element worked out on the side, let's see about integrating it with the Main Game Beta. The usual drill. Start with the Variables, and take this opportunity to clean up and group any stray Variable Declarations, with one exception: We will leave the var gameState = "Intro";
right at the top. Later, when you want to troubleshoot different levels you are working on, all you need to do is change the gameState to the current level and your Program will automatically boot into that level :) Then when you're ready to publish your game, set it back to "Intro"
and it will default to the Splash Screen.
Next copy over all your Functions. You could move the createSkeets();
function call into the Button on your Intro page if you like:
if(mouseIsPressed){
createSkeets();
gameState = "Main";
}
That way the Array won't be populated until needed. Since they won't be drawn to the screen until we tell them to, it doesn't matter a whole lot:
OK, so all we need to do now is add the calls to our Skeet Functions inside the case "Main":
Statement:
/** Call Skeet Functions */
updateSkeets();
drawSkeets();
and unless one of us has left something out somewhere, we should have a semi-functioning Game:
Next I would like to increase the Number of Enemies on each Level. So just for demo, we'll start out with only 1 Skeet on the "Main" level and then we'll add more on the following Levels.
First we'll change the default number of Objects from 4 to 1 at the top of our code: ```var numObjects = 1;``
We have to make it so each time a Skeet is deleted, it updates the count. We can let Agent Smith tally that up for us when he makes alterations;
var agentSmith = function(){
// Cycle through Array and check the list
for(var u = 0; u < skeets.length; u++) {
if(skeets[u].collision){ // If it bears the Mark
skeets.splice(u,1); // Terminate
numObjects -= 1; // Call it in to the home office
}
}
};
Then we'll make it so if you destroy all the enemies, it will advance you to the next Level and give you more things to shoot at:
if(numObjects === 0){
gameState = 1;
numObjects = 2;
}
if(playerLives === 0){
gameState = false;
}
Now looking ahead I can see that there are a whole bunch of function calls and assorted code that will be used on all the levels. Rather than having to copy and paste all of that into each new Case Statement for each new level, I will gather all of it up into one single Function which can be called one time on each level that executes all of the necessary code. To begin with, we are no longer going to need to display the number of lives on screen. We will however want to maybe display the Score. So at the top, let's declare a new Variable for the Score:
var playerScore = 0;
Now inside the case "Main":
Statement we will replace text(playerLives, 40, 20);
with text("Player Score: " + playerScore, 40, 20);
This will display some Text, followed by the printout of the Variable playerScore
. Right below that, we no longer need the text line that prints the keyCode to the screen, so we can delete that.
Then we'll head over to Agent Smith and see to it that every time he does his job there's a bonus in it for us:
var agentSmith = function(){
// Cycle through Array and check the list
for(var u = 0; u < skeets.length; u++) {
if(skeets[u].collision){
skeets.splice(u,1); // Terminate
playerScore += 1; // Add 1 to the score
numObjects -= 1;
}
}
};
Now we will Cut all the reusable code out of the case "Main":
Statement and put it into it's own Function:
var updateGame = function(){
/** Call Skeet Functions */
updateSkeets();
drawSkeets();
// Call Ball Functions
updateBall();
drawBall();
// Call Paddle Functions
updatePaddle();
drawPaddle();
// Print the Score Variable to the screen
text(playerScore, 40, 20);
};
Now we can go back to our "Main" Statement and call the Function there as well as on any other Levels that use the basic Gameplay. We can also delete any color info which was used to tell one level from another in the demo. Also our Ball and Paddle are being affected by the last color fill used by one of the Skeets. No problem. We'll just go into the Draw Paddle and Draw Ball Functions and add custom Color.
// Create the Draw Ball Function
var drawBall = function(){
fill(127, 80, 255);
ellipse(ballPositionX, ballPositionY, BALL_SIZE, BALL_SIZE);
};
var drawPaddle = function(){
fill(127, 0, 255);
rect(paddleLeft, PADDLE_TOP, PADDLE_WIDTH, PADDLE_HEIGHT);
};
When we get to the Game Over screen and hit the Replay Key, we want to set our number of Skeets back to the starting default. In this case 1.
default:
if(keyCode === 37){
playerLives = 3;
numObjects = 1;
gameState = "Main";
}
fill(255, 0, 0); // Red
text("Game Over\n\nPress the Left Arrow Key\nto start over", 90, 103);
break;
And if all went well, we should have a very rudimentary starter Game, and hopefully by now I have armed you with enough experience where you can customize it to your taste :)
If you have any questions or if I have overlooked anything in my descriptions, let me know. DillingerLee