TIMPIST
从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 很乐意提供它自己的 MouseMove
和 MouseClick
事件。
请注意,当线程被分离时,即使在这里只有两个线程运行,我们也无法再假设线程会同步。因此,如果游戏正在进行一些耗时的、不属于游戏玩法的事情 — 例如 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();
}
双缓冲
如果您的游戏板只是一个黑色的屏幕,那么双缓冲就不重要了。但是谁想在黑色的屏幕上玩游戏呢?如果您有一个不会改变的背景,并且有移动的部件,那么您将会
- 刷新屏幕,并且
- 不再处理闪烁和重绘所有内容。
这就是古老而受人尊敬的双缓冲模式的用武之地。在 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 日:发布。