[Updated July 31, 2020]
On the Solar2D forums someone asked about creating a Simon-like game where the computer highlights a color and the user touches the same one, then computer highlights two colors and user does the same, etc. Simon was one of the top electronic games in the 80s.
The question on the forum was along the lines of “how do I make a game like that?” which is pretty broad, and I’m not going to answer it here, but I played around with Solar2D yesterday and came up with something that could be the foundation for a game like that.
Here’s a 77-second video that gives you a quick look at what the game does:
It’s not a finished game, but it could certainly serve as the foundation for one. So let’s get started by diving into some code.
The Overview
In a nutshell, we’re going to create a table in Lua and populate it with a number of colors, all chosen at random.
Once we have that new sequence, the computer will run through it and highlight each colored ball (and play a sound) and then pass control over to the player.
The player will try to tape the balls in the same order. As they tap, a star or an X appears above the colored ball, depending on whether they’re right or wrong. That bit is just some eye-candy to make the game more interesting.
If the player taps all the balls in correct order, the win message is displayed, otherwise the lose message is displayed. In either case the player can start a new game at that time.
Creating A New Sequence
I started playing around with this tutorial code by by creating a routine that would make a new sequence for the user to solve. It looks like this:
1 2 3 4 5 6 7 8 9 |
local colorBalls = {"Blue", "Green", "Red", "Yellow"} local function createSequence(numColors) local sequence = {} for i = 1, numColors do sequence[#sequence+1] = colorBalls[math.random(1, #colorBalls)] end return sequence end |
Pass in the number of options you want in the sequence and it will grab that many colors at random and create a new table containing those options. At the end it will pass back that table, so you’ll call that function like this:
1 |
newSeq = createSequence(10) |
…and you’ll get back a table called newSeq that contains a random selection of 10 colors. Using a variable called sequenceIdx you can keep track of where you are in the sequence. For example,
1 2 |
sequenceIdx = 3 print ( newSeq[sequenceIdx]) |
…will print the third color in the sequence. Now you’re not going to be printing the value to the terminal, but you can still access it the same way. At the beginning of a turn you’ll set that variable to 1 and that will start with the color that’s in position 1 in the table.
Three Main Parts of the Program
Besides creating a sequence to be solved, the other main parts of the game are:
- Have the computer display the current sequence, one ball at a time.
- Allow the user to try and duplicate the sequence that was shown.
Some of the minor parts of the program including setting up the initial screen and allowing the user to play the game again after a win or loss.
Before we look at the code where the app shows the current sequence let’s take a peek at setting up the initial screen, because that sets up some code we’ll be using later.
That picture shows one ball larger and a bit brighter than the others — that’s what happens when the user touches the ball or when the app “chooses” it. It also plays a unique sound when chosen. Part of that happens because of the way the balls are set up at the beginning:
1 2 3 4 5 6 7 8 9 10 11 12 |
local function setUpBalls() for i=1, 4 do local idx = colorBalls[i] mainBalls[idx] = display.newImage("images/" .. colorBalls[i] .. ".png" ) mainBalls[idx].x = 60 * i + 10 mainBalls[idx].y = 90 mainBalls[idx].color = colorBalls[i] mainBalls[idx].alpha = .9 mainBalls[idx].sound = audio.loadSound("audio/" .. colorBalls[i] .. ".aiff") mainBalls[idx]:addEventListener ( "touch", ballTouched ) end end |
We create a loop that fills the mainBalls table with the info we need for each ball — the first thing we do is set the key for each value in the table to the color of the ball:
1 2 |
local idx = colorBalls[i] mainBalls[idx] = display.newImage("images/" .. colorBalls[i] .. ".png" ) |
The variable idx is set to Blue, Green, etc., which means that second line would look like this:
1 |
mainBalls["Blue"] = display.newImage("images/" .. "Blue" .. ".png" ) |
The only other lines of interest in that main loop are:
1 2 |
mainBalls[idx].alpha = .9 mainBalls[idx].sound = audio.loadSound("audio/" .. colorBalls[i] .. ".aiff") |
We’re setting the alpha to less than one so when we use the “pop” routine we can set it to 1 and that makes the ball brighten when it’s touched.
We also set the sound that should be played when the ball is chosen. By handling it this way we don’t have to programmatically decide which sound to play when a ball is touched, it’s automatic.
In fact, let’s look at that part of the code right now…
Popping the Balls
When the user touches a ball or the app chooses one, we want it to “pop” up and play a sound. To make it pop we scale it up and the back up at the same time increasing the alpha value.
1 2 3 4 5 6 7 |
local function popBall(b) local function unPop(b) transition.to ( b, { time=100, xScale = 1, yScale = 1, alpha = .9 } ) end transition.to ( b, { time=100, xScale = 1.2, yScale = 1.2, alpha = 1, onComplete=unPop} ) audio.play ( b.sound ) end |
We pass in a ball object (that was created in the setUpBalls() function), define the function that “unpops” the balls, and then go right into transition.to that makes the ball grow slightly. When it’s done it will call the unPop function that takes the ball back to normal.
You can see that we don’t need to figure out which sound to play, we just play the one that was saved in the sound property of the ball object. Set up it at the beginning and then never think about it again — that’s the best kind of programming. 🙂
It’s the Computer’s Turn
Okay, that’s a lot of the housekeeping out of the way, let’s look at the part where the app shows the sequence that the user will need to follow.
1 2 3 4 5 6 7 8 9 10 11 12 |
local function playSequence() popBall(mainBalls[newSeq[sequenceIdx]]) if sequenceIdx < #newSeq then timer.performWithDelay( BOOP_PAUSE, playSequence ) else GAME_STATE = "playing" sequenceIdx = 0 -- reset the idx so we can see if the player does it right local yourTurn = display.newText( "Your Turn...", centerX, centerY, "Helvetica", 36 ) yourTurn:fadeIn(2000, function() yourTurn:fadeOut(4000, true) end) end sequenceIdx = sequenceIdx + 1 end |
Before this function is called the sequenceIdx variable has been set to 1 to make sure we start at the beginning. The first thing we do is pop the ball that’s first in the sequence.
Remember that newSeq holds a list of colors such as: “Yellow”, “Green”, “Green”, “Red”, etc. so that when sequenceIdx equals 1 the popBall parameter could also be seen as:
1 |
popBall(mainBalls["Yellow"]) |
The if block that comes next checks to see if sequenceIdx is less than the number of values in the newSeq table. If it is, then it sets a timer to call the playSequence again after a set period of time (BOOP_PAUSE was set to 1000 at the beginning of the file) and then drops past the if block and increases the sequenceIdx by 1 so it points to the next value in the table.
Because of the timer that gets set every time, playSequence will continue to call itself until it has done a pop for each value in the sequence table. At that point the other part of the if block will come into play and we’ll show the player a message letting them know it’s their turn to play.
If you haven’t used the Crawl Space Library before you might be confused about this line:
1 |
yourTurn:fadeIn(2000, function() yourTurn:fadeOut(4000, true) end) |
When you use crawlspaceLib you automatically get the ability to fade objects in and fade them out. Not only graphics, but even text as you can see here.
Note: When this was originally written you needed 3rd-party libraries to do things like fading in and out, but those are now part of the official transition library in Solar2D. Check out the docs on that here:
https://docs.coronalabs.com/api/library/transition/index.html
Instead of setting up a callback to fade out the text after it’s faded in, I just combined the two. Now the message will fade in over 2 seconds and then fade out over 4 seconds.
You’ll also notice inside that if block we take care of two more things:
- Set the global GAME_STATE variable to “playing” (we’ll talk more about that later).
- Reset sequenceIdx so the user can start at the correct place when they start guessing — and we set it to zero since the next line after the if block will increment it. That way it will start at 1 for the user.
It’s the Player’s Turn
When we created the balls on the screen we added a touch event listener for each — inside that function we take care of all the stuff to determine whether then user touches the balls in the correct order. After checking to make sure the event.phase is “ended” we check to see if the game is actually in playing mode:
1 2 3 |
if GAME_STATE ~= "playing" then return end |
At the beginning of the code the GAME_STATE variable is set to “waiting” and doesn’t change to “playing” until after the computer’s turn is over. We could have enabled and disabled the event listener as needed, but I figured this way was easier — event listener is always on, but sometimes we just ignore the touch and return.
Next we pop the ball that was touched:
1 |
popBall(e.target) |
…and then look to see whether that ball was the correct one by comparing the current ball in the sequence to the color of the ball that was touched. Let’s look at the code that runs if the colors matched:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if newSeq[sequenceIdx] == e.target.color then local star = display.newImage("images/Star.png") star.x = e.target.x star.y = e.target.y - e.target.height star:fadeOut(1000, true) sequenceIdx = sequenceIdx + 1 -- check and see if we're done here with a win if sequenceIdx > #newSeq then GAME_STATE = "lost" audio.play ( WinnerSound ) local youWon = display.newText( "You Won!", centerX, centerY, "Helvetica", 36 ) showReset(youWon) end |
We briefly show a star above the ball that was touched and the increment the pointer (sequenceIdx) that shows us which value in the table we’re looking at. If the pointer is no greater than the number of values in the sequence, it means the user is done and we set the GAME_STATE (so touches will stop working), play the winning sound and display a message, and then put up a Start Game message so the user can play again if they like.
If the colors didn’t match we show an X above the ball and set the GAME_STATE to “lost”, play the losing sound and display the message, and then put up the Start Game message so the user can try again.
1 2 3 4 5 6 7 8 9 |
else local ex = display.newImage("images/X.png") ex.x = e.target.x ex.y = e.target.y - e.target.height ex:fadeOut(4000, true) GAME_STATE = "lost" audio.play ( LoserSound ) local youLost = display.newText( "You Lost! :(", centerX, centerY, "Helvetica", 36 ) showReset(youLost) |
In Summary
While this sample is a complete game, it’s not a “polished” game by any means. A “real” Simon game would start with one color in the sequence to copy, move on to two, then three, and so on until the user missed. A real game would also have a way to keep score such as the most colors in a sequence that have been matched, etc.
But there’s nothing stopping you from taking this code and building on it (although find your own ball graphics — the ones in this sample are licensed for this sample only, not for general distribution).
If you’d like to download the entire working sample project, join Game Dev Nation (It’s free!) and the download link will appear in this spot after you confirm your registration via email and log back in.
Please click the Like button below if you appreciate this tutorial. 🙂
Originally written for Corona SDK, which is now called Solar2D but still just as awesome.
nice work 😉
Awesome! Can’t seem to locate the download button for the project.
Great code Jay and a very well written tutorial!
Read the tut, studied the main.lua for 10 minutes and figured, what the heck, I’ll give this a try….I actually thought I’d have to create it’s own local function, or something much more complicated and referenced. Is Lua really this intuitive, is it the CrawlSpaceLib, Corona SDK or a combination?
When I first only added the “numBall = numBall +1” command (or is it a called a statement?) to the “won” function, the game restarted with the number of balls I failed at, almost a game save state. Proceeded to add the command in the “lost” function and was thrilled to see the results.
Not sure if this type of comment is desired, but for a 3 day noob, it was a great experience, thanks for facilitating it!
local numBalls = 2 — how many in the sequence?
***local numBalls = 2 + 1*** lol, 1st stab = FAIL
skip…
if sequenceIdx > #newSeq then
GAME_STATE = “won”
audio.play ( WinnerSound )
numBalls = numBalls + 1 –This stuff is awesome, I think I just got lucky on my 2nd stab!
local youWon = display.newText( “You Won!”, centerX, centerY, “Helvetica”, 36 )
showReset(youWon)
end
else
local ex = display.newImage(“images/X.png”)
ex.x = e.target.x
ex.y = e.target.y – e.target.height
ex:fadeOut(4000, true)
GAME_STATE = “lost”
audio.play ( LoserSound )
numBalls = 3
local youLost = display.newText( “You Lost! :(“, centerX, centerY, “Helvetica”, 36 )
showReset(youLost)
end
BTW, The sound files don’t play on my Win7 laptop either, not sure why.
[…] Match Game Magic I needed to shuffle picture objects between each level. In this article… Create A Simon Game in Corona SDK | Game Dev Nationhttp://gamedevnation.com/game-development/create-a-simon-game-in-corona-sdk/On the Corona SDK forums […]
I can’t download the code =(
Why can’t you? Do you get an error message or just not see the link? If you don’t see the link, make sure you register as a member of GDN and then verify your membership by clicking the link in an email you’ll receive — if you didn’t get a verification email check your spam/junk folder in case it got waylaid.
Jay
Hi Jay
Great tutorial, I’m having trouble downloading the code – I’ve registered and tried logging in with multiple browsers but I can’t seem to get the code link.
i registered and verified through my email but it still asks me to register instead of the download source button 🙁 and iv logged in and out a few times!!
Same problem here … where is this download link please 🙂
After you register you will receive an email — you must click the link in that email before you can log in. (If you don’t see the email, check your junk/spam folder.)
Once you have verified who you are by clicking that link, you can log in and see the download link on the page.
If you have done all that and can’t see the download link, send me your login info (using the Contact form on this site) and I’ll see if I can track down what’s going on.
Jay
Same here
Im logged in and it wants me to register…..i already confirmed 2 days ago too…something wrong with he code on this website if so many ppl are having the same problem.
is there anywhere else i can download this sample file?
In most of the cases where people are having problems they trace back to user-error (but admittedly, not always). Or a browser that has the “you’re not a member page” cached. So a forced refresh (Ctrl+F5 on Windows, Shift+Reload on Mac) will usually do the trick. Give that a shot and see if things snap back to normal. Sorry for the hassle.
I want to use much bigger buttons and position them in a 2×2 pattern, but I have not idea how since each button is not positioned individually. Anyone have any clue how to set an independent x and y value for Red, Green, Yellow, and Blue?
Even though each button isn’t positioned individually, that doesn’t mean you can’t do it yourself. Two ways to handle that: inside the setUpBalls() function position each ball as it’s created. Or, after they are created, move them to wherever you like. Using mainBalls[1].x = 10; mainBalls[1].y = 20; will reposition the first ball. Then you can use mainBalls[2], mainBalls[3], and mainBalls[4].
Following your suggestions, I tried commenting out a few lines of code and adding the code below to position each ball separately. I obviously did something wrong since my actions broke the app. Any feedback would be much appreciated. 😉
— display the four balls on the screen and tweak their parameters
local function setUpBalls()
for i=1, 4 do
local idx = colorBalls[i]
mainBalls[idx] = display.newImage(“images/” .. colorBalls[i] .. “.png” )
mainBalls[1].x = 10; mainBalls[1].y = 20;
mainBalls[2].x = 20; mainBalls[2].y = 30;
mainBalls[3].x = 30; mainBalls[3].y = 40;
mainBalls[4].x = 40; mainBalls[4].y = 40;
–mainBalls[idx].x = 60 * i + 10
–mainBalls[idx].y = 90
–mainBalls[idx].alpha = .9
–mainBalls[idx].color = colorBalls[i]
mainBalls[idx].sound = audio.loadSound(“audio/” .. colorBalls[i] .. “.aiff”)
mainBalls[idx]:addEventListener ( “touch”, ballTouched )
end
end
The mainBalls[1-4] pbjects don’t exist until after that for loop is done. The first time through it’s creating the mainBalls[1] object, second time through the mainBalls[2] object, etc.
If you cut out the following four lines and paste them after the loop (in between those two end statements) it will probably work:
See if that helps.
Jay
Hi Jay. Thanks for the fast reply. Is this what you meant? If so, the something else is needed. The text disappears and only the yellow ball shows and its in the very upper left corner of the screen.
— display the four balls on the screen and tweak their parameters
local function setUpBalls()
for i=1, 4 do
local idx = colorBalls[i]
mainBalls[idx] = display.newImage(“images/” .. colorBalls[i] .. “.png” )
–mainBalls[idx].x = 60 * i + 10
–mainBalls[idx].y = 90
mainBalls[idx].alpha = .9
mainBalls[idx].color = colorBalls[i]
mainBalls[idx].sound = audio.loadSound(“audio/” .. colorBalls[i] .. “.aiff”)
mainBalls[idx]:addEventListener ( “touch”, ballTouched )
end
mainBalls[1].x = 10; mainBalls[1].y = 20;
mainBalls[2].x = 20; mainBalls[2].y = 30;
mainBalls[3].x = 30; mainBalls[3].y = 40;
mainBalls[4].x = 40; mainBalls[4].y = 40
end
Jerry, go to the following page and send me the entire code from that file:
http://outlawgametools.com/contact/
I’ll take a look at it in a bit and see if I can spot the problem.
One thing I see is that your objects will all be overlapped — moving things 10 pixels away from each other isn’t going to work very well. I think the balls themselves are 60 pixels in diameter.
I sent the code the the above link. Thanks for looking at this.
Jerry
I sent the code the the above link.
Jerry