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

TIMPIST

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (3投票s)

2012 年 12 月 16 日

CPOL

9分钟阅读

viewsIcon

21726

downloadIcon

313

从CoCo-ads回到未来。

引言

TIMPIST — 令人震惊的是 — 是一款《雷神之锤》类的游戏。James Wood 在 Microsoft 的 Extended BASIC 中设计了它……在 1986 年。这篇文章将其移植到了 C# 2.0。本文介绍了诸如线程和双缓冲之类的游戏概念。此外,我还提供了一个 CoCo 的 Extended-BASIC DRAW 命令的解析器,并且我借鉴了 Colin Angus Mackay 的“Beep”函数。所有这些都应该对那些 DeLorean 仍在保修期内的“年长宅男”们有所帮助。

背景

TIMPIST 本身在主屏幕上进行了说明

(是的,“BUTTON !! 发射!” 是原始文本的一部分。我希望它能成为一个梗。)

所以 — 1986 年!我们不都怀念那段时光吗?那时你有力量,Pet Shop Boys 有头脑,Michael Douglas 拖着一个巨大的手机穿梭于华尔街。同样在 1986 年,成千上万的业余程序员致力于为家用电脑提供街机体验。仅 TRS-80 Color Computer 就有 Color Computer News、Color Computer Magazine 以及 — 坚持到最后 — 免费新闻通讯 CoCo-Ads。“其中”的“广告”不仅仅是产品的广告;程序员会通过提交自己的作品、以自己的名字署名,并通常附上自己的邮寄地址来打广告自己。(以某种方式,这很像 CodeProject。)

TIMPIST 就是这样一款产品,它出现在 1986 年 9 月的 CoCo-Ads 期刊上,而那期可能是我收到的最后一期。我之所以选择这款游戏,是因为在 1986 年,我花了几个小时才把它全部打完,然后只玩了大约十五分钟,接着就关掉了电脑 — 忘记先保存游戏。不久之后,我们有了一台新电脑,所以不可能重新做所有工作。哇!我还没完成! 这么多年过去了,一直困扰着我,我没有完成这款游戏。在我拥有的所有电脑杂志中,我保留了这一本,就是因为这个原因。

现在我终于捕获到了(黑与)白鲸。它已经被移植了 — 它已经“完成”得差不多了。这次经历是值得的;它教会了我很多关于游戏设计的知识。

使用代码

最简单的使用方法就是下载“TIMPIST.exe.zip”,将文件放到某个文件夹里,运行可执行文件“.exe”然后尽情享乐。太棒了!

哦,等等,我们是在 *Code*Project 上。应该是涉及代码的。哦,好吧。(扫兴鬼。)

我用 Visual Studio 2012 编写了此代码。解决方案可能针对 .NET 4.5,但项目针对 .NET 2.0。因此,如果您使用的是早期版本的 Visual Studio,则可能需要创建一个新解决方案并将这里提供的三个 vbproj 文件附加到其中。

至于解决方案中的三个项目。其中一个是游戏本身,完全归功于 James W Wood,因为在这里我只是他的代码猴子。另一个是音乐库,我们的音乐猴子是Colin Angus Mackay。第三个是我的贡献,一个用于 Extended-BASIC“DRAW”命令的解析器。我先从我自己的部分开始。

DRAW ""

Color Computer 的 RAM 不足以支持如今细节丰富的街机图形。16K+ 版本在其“Extended BASIC”库中提供了绘制简单小精灵的方法。这个方法就是 DRAW 命令。该命令会解析一个字符串并跟踪画笔的状态。年长的宅男们会立即认出海龟绘图系统,如果不是从 CoCo,至少也是从 LOGO 认出的。

如今,运行时精灵已不再那么重要,因为我们可以直接在 Paint 中绘制图形并截屏。但我们这些怀旧爱好者会需要它……

所以,万一您没有 DeLorean,请先将 CoCoDrawParser 项目添加到您的解决方案中。可以通过“添加引用”来完成。

要设置状态,请实例化一个 TurtleDrawer。它必须与一个 Graphics 对象关联。我还要求一个前景色画笔对象。在此示例中,此画笔在整个游戏中都相同,因此是常量(好吧,是只读静态的)。

private static readonly Pen PEN_FORE = new Pen(new SolidBrush(Color.White));
...
CocoDrawParser.TurtleDrawer td = new TurtleDrawer(thisGraphics, PEN_FORE);

然后,绘制字符串。以下代码将抬起画笔,将画笔设置在 (30,30),放下画笔,缩放到“4”,然后向右下、左上绘制。这是一个矩形。

td.Draw("BM30,30S4R195D135L195U135");

对于解析的命令,我包含的不仅仅是这款游戏所必需的。如果,真主保佑,我将更多游戏移植到 .NET,我会补全其余部分。或者,你们中的任何一个人都可以做到!

为了进入解析器本身,我使用了一个策略模式(带有状态模式的暗示)。我将每个命令保存在自己的函数中。首先,我定义了标准的函数格式,它总是接收一个字符串输入;然后,我定义了一个字符到委托的哈希表。

private delegate void DrawFunc(string s);
private Dictionary<char,DrawFunc> _parser;

在实例化时,我将字符分配给相应函数。

_parser = new Dictionary<char,DrawFunc>()
{
    {'M', new DrawFunc(Move)},
...
}

Draw 命令接收必须解析的字符串。我遍历该字符串。一旦我确定下一个命令是什么,我就会获取它的参数并调用它的函数。

_parser[command](strarg);

[所以,您可能会问 — 为什么不直接使用 C 和 VB 语言中现有的 switch() 语句呢?我的回答是,我……不喜欢 switch()。尤其是在代码的很大一部分围绕着将函数分配给条件的情况下。策略模式迫使程序员编写自文档化的函数。我们可以立即看到将有一个“Move”命令,并且我们可以找到它在 _parser 中的代码。]

多线程

人们很快就会发现,CoCo 在面向对象甚至结构化编程方面提供的功能非常有限。但人们也可以发现像这样的动作游戏之间普遍存在的共同点。

任何动作游戏都有两个竞争对手:玩家和敌人。玩家自行其是,而敌人则众所周知地自行其是……

《雷神之锤》类的游戏要简单一些,因为敌人仅仅是时钟。当玩家面对那个“敌人”时,只需在正确的地方“ BUTTON !! 发射!”;该操作会重置时钟。这使我们不必过多关注敌人的线程。

同时,在这种尺寸如此小的游戏中,避免过度设计也很重要。我的意思是,这东西最初只是在一个免费新闻通讯中发表的两页内容,天哪。

为了处理敌人 — “武器” — Timer_Tick 事件会按设定的时间触发,并增加一个计数器。当计数器达到最大值(此处为 5)时,游戏将通过 LoseLife() 例程惩罚玩家。

private void timer1_Tick(object sender, EventArgs e)
{
    if (_weapon.AtEnd())//5
        LoseLife();
    else
    {
        Song playG = new Song(DEFAULT_COCO_TEMPO);
        //it just said play G. quarter-notes are default.
        playG.Notes.Add(new Note(Duration.Quarter, Pitch.G, DEFAULT_OCTAVE));
        playG.Play();
        RefreshBoard();
        _weapon.IncrementThreat();
    }
}

为了处理玩家的操作,最好为玩家的控制创建监听器 — 或者,更好的是,找到那些已经实现的监听器 — 并对玩家的事件做出反应。玩家的场地是 picturebox。picturebox 很乐意提供它自己的 MouseMoveMouseClick 事件。

请注意,当线程被分离时,即使在这里只有两个线程运行,我们也无法再假设线程会同步。因此,如果游戏正在进行一些耗时的、不属于游戏玩法的事情 — 例如 LoseLife() 中的爆炸序列 — 那么在进行这些操作时,游戏应该停止计时器。

更隐蔽的是,当玩家仍在移动鼠标时,敌人可能已经获胜。我发现在从玩家角度刷新屏幕时会发生这种冲突。因此,在玩家的事件处理程序中,我插入了代码来检查敌人是否尚未获胜,然后再刷新屏幕。

这是 MouseMove 的实现方式。我将玩家限制在场地边缘。如果玩家的鼠标位于棋盘的正确部分,我就会接受该决定到 _player 对象中。

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    if (_backBuffer != null)
    {
        int x = e.X;
        int y = e.Y;

        //the box is 20,20 - 230,170 

        if (x > 210)
            x = 230;
        else if (x < 40)
            x = 20;

        if (y > 150)
            y = 170;
        else if (y < 40)
            y = 20;

        if (((x != _player.x) || (y != _player.y))
            && ((x == 20) || (x == 230) || (y == 20) || (y == 170)))
        {
            _player.x = x;
            _player.y = y;
            if (!_weapon.AtEnd())
                RefreshBoard();
        }
    }
}

这就是 MouseClick 的处理方式。(还记得“AWinnerIsYou”吗?我将在双缓冲部分再次提到它。)

private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
{
    _g.DrawLine(PEN_FORE, _player.x + 2, _player.y + 2, 127, 97);
    Song playCEA = new Song(DEFAULT_COCO_TEMPO);
    playCEA.Notes.Add(new Note(Duration.Quarter, Pitch.C, DEFAULT_OCTAVE));
    playCEA.Notes.Add(new Note(Duration.Quarter, Pitch.E, DEFAULT_OCTAVE));
    playCEA.Notes.Add(new Note(Duration.Quarter, Pitch.A, DEFAULT_OCTAVE));
    playCEA.Play();

    if (_player.InSector(_weapon.Sector))
    {
        AWinnerIsYou();
        RefreshBoard();
    }
    else if (!_weapon.AtEnd())
        RefreshBoard();
}

双缓冲

如果您的游戏板只是一个黑色的屏幕,那么双缓冲就不重要了。但是谁想在黑色的屏幕上玩游戏呢?如果您有一个不会改变的背景,并且有移动的部件,那么您将会

  1. 刷新屏幕,并且
  2. 不再处理闪烁和重绘所有内容。

这就是古老而受人尊敬的双缓冲模式的用武之地。在 Color Computer 中,这是通过 Paging 完成的,游戏会通过 PMODE 在页面之间切换。在 .NET 2.0 中,我们捕获一个图形矩形并将其存储在 BufferedGraphics 中。

private BufferedGraphics _backBuffer;

这个概念是分离出那些每时每刻都在变化的东西 — 您和敌人 — 然后将背景的东西放入一个缓冲区。 

_backBuffer = MakeBoard(BufferedGraphicsManager.Current);

private BufferedGraphics MakeBoard(BufferedGraphicsContext currentContext)
{
    BufferedGraphics PMODE3 = currentContext.Allocate(_g, pictureBox1.DisplayRectangle);
    Graphics backGraphics = PMODE3.Graphics;


    CocoDrawParser.TurtleDrawer td = new TurtleDrawer(backGraphics, PEN_FORE);

    td.Draw("BM30,30S4R195D135L195U135");//outer box
    td.Draw("BM112,88R28D20L28U20");//inner box
...
    return PMODE3;
}

当您和/或敌人移动时,游戏会将缓冲区覆盖到您旧的位置上,就像我在 RefreshBoard() 中所做的那样。

_backBuffer.Render(_g);

并重绘移动的部件。 

后缓冲区也可以改变,但预期不会经常改变。因此,当需要写入分数和生命值时,会将它们写入该缓冲区。这是增加分数的示例,“AWinnerIsYou”。

private void AWinnerIsYou()
{
    timer1.Stop();

    Song.Beep(2217 >> 2, 1 << 9);

    _score += (_weapon.WhichOne + 1) * 10;//Q is zero-based

    _backBuffer.Graphics.FillRectangle(PEN_BACK.Brush,
        110, 3, 91, 11);
    WriteScore();

    _weapon = Weapon.GenerateNew();

    if (_score > 500)
        timer1.Interval = 600;
    else if (_score > 1000)
        timer1.Interval = 300;

    RefreshBoard();
    timer1.Start();
}

稍后当缓冲区被重新覆盖时,新信息就会在那里。

兴趣点 

我印象深刻的是,双缓冲的概念这么早就存在了。PMODE 没有时间闪烁!

我遇到的最大困难并不是编写 DRAW 解析器。那实际上还挺有趣的。

最大的困难是*声音*。Extended BASIC 允许您在字符串中播放一系列音符。.NET 没有原生的方法可以做到这一点。我找不到任何真正的 .NET 方法来简单地访问声音 — 就像在 Extended BASIC 中那样,只需快速播放“音符 A,音符 B”的字符串即可完成 (就像 Extended BASIC 中一样)。我到处寻找。然后我说“去他妈的”,就从 Mackay 的 .NET 1“Beep”项目中偷了四个类,我希望他不会起诉我。

历史 

  • 2012 年 12 月 14 日:发布。
© . All rights reserved.