Introduction
I once stepped on Clickomania next generation from Matthias Schüssler and liked the game so much that I decided to implement that game in C# from scratch. My version is simpler but uses a lot of the same design. I tested the game on an Athlon 2000+ and it seemed to work smoothly.
Features
- Animations and sounds
- Save the high scores (Top 10)
- Take back move
The Object Model
I use two big classes, MainForm
and Engine
. The MainForm
deals with events, drawing and animation, the Engine
contains the logic. I also use three struct
s, one for the Scores
(Score, Player name, Date, etc.):
public struct Scores
{
public int _iScores;
public string _sNames;
public int _SbestMove;
public int _SballsLeft;
public int _Stime;
public string _Date;
public int _sTakeBacks;
}
One for the Ball
:
public struct Ball
{
public int _icolor;
public bool _exists;
public bool _isDisappearing;
public int _vel;
public int _yvel;
}
and one for the Turn
, which contains information about the disappeared balls, their position and the moved columns, so the moves can be taken back:
public struct Turn
{
public int _itColor;
public int [] _itposX;
public int [] _itposY;
public int [] _column;
}
A Bit About the Logic
On the PictureBox
, the balls seem to move, but actually they don't. The balls array stores 8x12 balls. They stay at their place on the board. They only change color or get the status _exist = false
when they are supposed to be gone.
When the player clicks on the ball, it checks if it belongs to a group of balls with the same color. This is done with a recursive function:
private int CheckNextBall(int x, int y, int color)
{
int px, nx, py, ny;
px = (x == 0 ? 0 : x - 1);
nx = (x == 7 ? 7 : x + 1);
py = (y == 0 ? 0 : y - 1);
ny = (y == 11 ? 11 : y + 1);
int ret = 1;
if((_ball[px, y]._icolor == color) && (_ball[px, y]._exists) && (px != x))
{
_ball[px, y]._exists = false;
_ball[px, y]._isDisappearing = true;
_turn[_turn.GetLength(0) - 1]._itColor = color;
_X[_index] = px;
_Y[_index] = y;
_index++;
ret += CheckNextBall(px, y, color);
}
if((_ball[nx, y]._icolor == color) && (_ball[nx, y]._exists) && (nx != x))
{
_ball[nx, y]._exists = false;
_ball[nx, y]._isDisappearing = true;
_turn[_turn.GetLength(0) - 1]._itColor = color;
_X[_index] = nx;
_Y[_index] = y;
_index++;
ret += CheckNextBall(nx, y, color);
}
if((_ball[x, py]._icolor == color) && (_ball[x, py]._exists) && (py != y))
{
_ball[x, py]._exists = false;
_ball[x, py]._isDisappearing = true;
_turn[_turn.GetLength(0) - 1]._itColor = color;
_X[_index] = x;
_Y[_index] = py;
_index++;
ret += CheckNextBall(x, py, color);
}
if((_ball[x, ny]._icolor == color) && (_ball[x, ny]._exists) && (ny != y))
{
_ball[x, ny]._exists = false;
_ball[x, ny]._isDisappearing = true;
_turn[_turn.GetLength(0) - 1]._itColor = color;
_X[_index] = x;
_Y[_index] = ny;
_index++;
ret += CheckNextBall(x, ny, color);
}
return ret;
}
This function checks the color of the four neighboring balls, and if they have the same color, sets its state to disappearing and checks the neighbors for their colors. It also saves the information about color and position in the Turn
struct
, so that the player can take back the move if he wants to.
Animation
This is done in the Mainform
class using global variables of position and size of animated balls, and the pictureBox.Refresh()
method. For example, to make balls appear or disappear, do the following:
public void MakeBallsAppear(bool appear)
{
if(!appear)
{
PlayWav(1);
for(int i = 0; i < 12; i++)
{
_var = i;
Thread.Sleep(10);
this.pictureBox1.Refresh();
}
_var = 0;
}
else
{
PlayWav(8);
for(int i = 11; i >= 0; i--)
{
_var = i;
Thread.Sleep(10);
this.pictureBox1.Refresh();
}
}
}
The Paint
event draws one frame when pictureBox.Refresh()
is called in the loop above. We also wait for 10 ms before painting the next frame, so that the animations wouldn't appear faster on faster machines:
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
for(int i = 0; i < 8; i++)
{
for(int j = 0; j < 12; j++)
{
if(engine._ball[i, j]._exists)
{
g.DrawImage(_bmp[engine._ball[i, j]._icolor],
(i * 24) - _posx * engine._ball[i, j]._yvel,
(j * 24) + _posy * engine._ball[i, j]._vel,
24, 24);
}
if(engine._ball[i, j]._isDisappearing)
{
g.DrawImage(_bmp[engine._ball[i, j]._icolor],
(i * 24) + _var, (j * 24) + _var,
24 - (_var*2), 24 - (_var*2));
}
}
}
if((_gameover) && (!_gamewon))
{
g.DrawString("GAME OVER", new Font("Arial", 20),
System.Drawing.Brushes.Black,
new Point(10, 122));
g.DrawString("GAME OVER", new Font("Arial", 20),
System.Drawing.Brushes.LightBlue,
new Point(8, 120));
}
if(_gamewon)
{
g.DrawString("GAME WON", new Font("Arial", 20,
System.Drawing.FontStyle.Bold),
System.Drawing.Brushes.Black,
new Point(12, 122));
g.DrawString("GAME WON", new Font("Arial", 20,
System.Drawing.FontStyle.Bold),
System.Drawing.Brushes.Yellow,
new Point(10, 120));
}
}
Sounds
The PlaySound(...)
method from winmm.dll is used to play with WAV files:
[DllImport("winmm.dll")]
private static extern bool PlaySound( string lpszName,
int hModule, int dwFlags );
private void PlayWav(int play)
{
if(checkBox1.Checked)
{
string myFile = ".\\Sounds\\default.wav";
switch(play)
{
case 1:
myFile = ".\\Sounds\\BallDisappear.wav";
break;
case 2:
myFile = ".\\Sounds\\BallDown.wav";
break;
case 3:
myFile = ".\\Sounds\\lost.wav";
break;
case 4:
myFile = ".\\Sounds\\newgame.wav";
break;
case 5:
myFile = ".\\Sounds\\ColumnDis.wav";
break;
case 6:
myFile = ".\\Sounds\\ColumnAppear.wav";
break;
case 7:
myFile = ".\\Sounds\\BallUp.wav";
break;
case 8:
myFile = ".\\Sounds\\BallAppear.wav";
break;
case 9:
myFile = ".\\Sounds\\Won.wav";
break;
case 10:
myFile = ".\\Sounds\\illegal.wav";
break;
default:
break;
}
PlaySound(myFile, 0, 0x0003);
}
}
The WAV files have to be in the subfolder Sounds of the folder containing Clickmania.exe. I have used some sounds of the game Half Life 2. If you want to use your own sounds, you can upload them to the sounds folder and rename them.
Scores
All scores are stored in a binary file called Scores.sco in the same folder as the exe. If this file does not exist, it will be generated when the game is run. This file also contains the name of the last user who reached the top 10, so he shouldn't reenter his name if he reaches the top10 again. To view the high scores, I have used the ListView
control:
private void PopulateListbox()
{
string name, score;
listView1.Items.Clear();
ListViewItem [] items = new ListViewItem[10];
Color textColor = new Color();
Font font;
for(int i = 0; i < 10; i++)
{
name = (i + 1).ToString() + ":" +
" " + _MF._stScores[i]._sNames;
score = _MF._stScores[i]._iScores.ToString();
if (i == 9)name = (i + 1).ToString() + ":" +
" " + _MF._stScores[i]._sNames;
textColor = _MF._stScores[i]._SballsLeft == 0 ?
System.Drawing.Color.Blue : System.Drawing.Color.Black;
textColor = (_MF._stScores[i]._sTakeBacks == 0 &&
_MF._stScores[i]._SballsLeft == 0) ?
Color.DarkRed : textColor;
font = (textColor == Color.Black) ?
new Font("Arial", 8) : new Font("Fixedsys", 8,
(_MF._stScores[i]._sTakeBacks == 0) ?
FontStyle.Italic : FontStyle.Regular);
items[i] = new ListViewItem(new string[] {name, score,
_MF._stScores[i]._Date,
_MF._stScores[i]._Stime.ToString(),
_MF._stScores[i]._SbestMove.ToString(),
_MF._stScores[i]._SballsLeft.ToString(),
_MF._stScores[i]._sTakeBacks.ToString()},
-1, textColor, Color.White, font);
listView1.Items.Add(items[i]);
}
}
Information is shown in a different color according to how the game was played, for example if the player left no balls or if he took no moves back.
About the Program
This is one of my first C# implementations, so be indulgent if my code seems junk at some places.
License
This article has no explicit license attached to it, but may contain usage terms in the article text or the download files themselves. If in doubt, please contact the author via the discussion board below.
A list of licenses authors might use can be found here.