65.9K
CodeProject 正在变化。 阅读更多。
Home

Clickmania 游戏

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.13/5 (4投票s)

2005 年 3 月 31 日

3分钟阅读

viewsIcon

57129

downloadIcon

2331

一个简单的逻辑游戏

引言

我曾经玩过Matthias Schüssler制作的Clickomania 新一代,非常喜欢这款游戏,因此决定用C#从头开始实现它。我的版本更简单,但使用了许多相同的设计理念。我在Athlon 2000+上测试了这款游戏,运行起来非常流畅。

特点

  • 动画和声音
  • 保存高分(前10名)
  • 撤销移动

对象模型

我使用了两个主要的类,MainFormEngineMainForm处理事件、绘图和动画,Engine包含游戏逻辑。我还使用了三个struct结构体,一个用于Scores(分数、玩家姓名、日期等),

public struct Scores
{
    public int _iScores;    // score
    public string _sNames;  // player name
    public int _SbestMove;  // his best move
    public int _SballsLeft; // balls left after game over
    public int _Stime;      // time for that game
    public string _Date;    // the date he played (mm/dd/yyyy mm:ss)
    public int _sTakeBacks; // how many times did he take back 
                            // during the game
}

一个用于Ball(球),

public struct Ball
{
    public int _icolor;          // its color
    public bool _exists;         // does it still exist
    public bool _isDisappearing; // is it in animation status
    public int _vel;             // velocity of animation
    public int _yvel; 
}

还有一个用于Turn(回合),其中包含关于消失的球、它们的位置和移动的列的信息,以便玩家可以撤销移动。

public struct Turn
{
    public int _itColor;   // Color of disappeared balls
    public int [] _itposX; // Position
    public int [] _itposY;
    public int [] _column; // Disappeared columns
}

关于游戏逻辑

PictureBox上,球看起来像是移动的,但实际上它们并没有移动。球数组存储8x12个球。它们在棋盘上的位置保持不变。只有当它们应该消失时,它们才会改变颜色或将status _exist设置为false

当玩家点击一个球时,它会检查该球是否属于一组颜色相同的球。这是通过一个递归函数完成的。

private int CheckNextBall(int x, int y, int color)
{
    int px, nx, py, ny;
    px = (x == 0 ? 0 : x - 1);  //prior X
    nx = (x == 7 ? 7 : x + 1);  //next X
    py = (y == 0 ? 0 : y - 1);  //prior Y
    ny = (y == 11 ? 11 : y + 1);//next Y
    int ret = 1;
    // the 4 "if" statements do basically the 
    // same, they check if the 
    // neighbour of the actual ball has the same color
    if((_ball[px, y]._icolor == color) && (_ball[px, y]._exists) && (px != x))
    {
        // make it inexistant
        _ball[px, y]._exists = false;
        // to animate it in the main form 
        _ball[px, y]._isDisappearing = true;
        // save color of ball( so we can take the turn back) 
        _turn[_turn.GetLength(0) - 1]._itColor = color;
        _X[_index] = px;        // and save its position (for the same reason)
        _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;
}

此函数检查四个相邻球的颜色,如果它们颜色相同,则将其状态设置为消失,并检查相邻球的颜色。它还会将颜色和位置信息保存在Turn结构体中,以便玩家可以根据需要撤销移动。

动画

这是在Mainform类中使用动画球的位置和大小的全局变量以及pictureBox.Refresh()方法完成的。例如,要使球出现或消失,请执行以下操作:

/// <summary>
/// Balls Disappear or appear
/// </summary>
/// <param name="appear"> if they are appearing or disappearing</param>
public void MakeBallsAppear(bool appear)
{
    if(!appear)
    {
        PlayWav(1); // play a sound
        // redraw all balls, but animate only thosse with appear state
        // this occurs when balls that are clicked disappear
        for(int i = 0; i < 12; i++)
        {
           _var = i;
           Thread.Sleep(10); // sleep 10 ms between 2 frames
           this.pictureBox1.Refresh();
        }
        _var = 0;
    }
    else
    {
        PlayWav(8);
        // same thng here too, but balls reappaer when 
        // player takes a move back
        for(int i = 11; i >= 0; i--)
        {
            _var = i;
            Thread.Sleep(10);
            this.pictureBox1.Refresh();
        }
    }
}

Paint事件在上述循环中调用pictureBox.Refresh()时绘制一帧。我们还在绘制下一帧之前等待10毫秒,这样动画就不会在速度更快的机器上显示得更快。

/// <summary>
/// here all the drawing takes place. for each 
/// frame, this event is called once, 
/// with a different _var, _posx or _posy 
/// if an animation takes place
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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))
    {
        // shadow of the text
        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)
    {
        // shadow of the text
        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));
    }
}

音效

PlaySound(...)方法来自winmm.dll,用于播放WAV文件。

[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); // Play the sound
    }
}

WAV文件必须位于包含Clickmania.exe的文件夹的Sounds子文件夹中。我使用了一些Half Life 2游戏的声音。如果要使用自己的声音,可以将它们上传到sounds文件夹并重命名。

分数

所有分数都存储在一个名为Scores.sco的二进制文件中,该文件与exe文件位于同一文件夹中。如果该文件不存在,则在运行游戏时会生成它。此文件还包含最后进入前10名的用户的姓名,因此如果他再次进入前10名,则不需要重新输入他的姓名。为了查看高分,我使用了ListView控件。

private void PopulateListbox()
{
    string name, score;
    listView1.Items.Clear(); // remove items in listview
    ListViewItem [] items = new ListViewItem[10];
    //DateTime date;
    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]);
    }
}

根据游戏的进行方式,信息将以不同的颜色显示,例如,如果玩家没有留下任何球或没有撤销任何移动。

关于程序

这是我的第一个C#实现之一,如果我的代码在某些地方看起来很糟糕,请多包涵。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.