提示(译文)
By robot-v1.0
本文链接 https://www.kyfws.com/games/timpist-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 13 分钟阅读 - 6040 个词 阅读量 0提示(译文)
原文地址:https://www.codeproject.com/Articles/508016/TIMPIST
原文作者:Zimriel
译文由本站 robot-v1.0 翻译
前言
Back to the future from the CoCo-ads.
从CoCo-ads回到未来.
介绍(Introduction)
TIMPIST是-令人震惊-(TIMPIST is - shockingly - a) 暴风雨(Tempest) 类似的游戏.一个詹姆斯`伍德(James Wood)在微软的Extended BASIC中设计了它. . .在1986年.它在这里是C#2.0的移植端口.本文介绍了游戏概念,例如线程和双缓冲.另外,我为CoCo的Extended-BASIC DRAW命令提供了一个解析器,并且剥去了Colin Angus Mackay的"提示音"功能.所有这些都应对DeLoreans仍在保修期内的任何其他Nerds Of Ages有帮助.(-like game. One James Wood designed it in Microsoft’s Extended BASIC . . . in 1986. It is here presented as a port to C# 2.0. This article introduces gaming concepts like threading and double-buffering. Also I offer a parser for the CoCo’s Extended-BASIC DRAW command, and I rip off Colin Angus Mackay’s “Beep” function. Any and all of this should be of help to any other Nerds Of Age whose DeLoreans are still under warranty.)
背景(Background)
TIMPIST本身在前屏幕中进行了解释:(TIMPIST itself is explained in the front screen:)
(是的,“用按钮射击!!“是原文的一部分.我希望它成为一个模因.)((Yes, “Fire with BUTTON !!” was part of the original text. I am hoping that it becomes a meme.)) 所以-1986年!难道我们都错过那些日子吗?当您拥有坚强的头脑时,“宠物店男孩"有大脑,而迈克尔`道格拉斯在华尔街拖了一部巨大的手机.还是在1986年,成千上万的业余编码员为家用计算机提供了街机体验.仅对于TRS-80彩色计算机,我们就有彩色计算机新闻,(So - 1986! Don’t we all miss those days? when you had the brawn, the Pet Shop Boys had the brains, and Michael Douglas towed a massive cell phone through Wall Street. Also in 1986, thousands of amateur coders were out to provide an arcade experience for the home computer. For the TRS-80 Color Computer alone, we had Color Computer News,) 彩色计算机杂志(Color Computer Magazine) 和-拖到痛苦的尽头-免费电子报(and - dragging on to the bitter end - the free newsletter) 可可广告(CoCo-Ads) .其中的"广告"不仅仅是产品广告.编码员会做广告(. The “ads” therein weren’t just ads for products; coders would advertise)他们自己(themselves),以自己的名字(通常包括他们的邮寄地址)提交自己的作品. (就其方式而言,它很像CodeProject.)(, by submitting their own work under their own names and usually including their mailing addresses. (It was much like CodeProject in its way.)) TIMPIST就是这样一种产品,该游戏最终发行于1986年9月的CoCo-Ads中,而该问题可能是发给我的最新一本.我之所以选择这款游戏,是因为在1986年,我花了数小时才全部输入,然后只玩了15分钟,然后关闭了计算机,却忘记了先保存游戏.不久之后,我们有了一台新计算机,毫无疑问要重做所有工作.哇!(One such offering was TIMPIST, which game ended up in the September 1986 issue of CoCo-Ads and which issue might have been among the last issues sent to me. The reason I’ve picked on this game is that, in 1986, I spent hours typing it all in, and I only got to play it for about fifteen minutes, and then I turned off the computer - forgetting to save the game first. And not long after that we got a new computer and there was no question of redoing all my work. Waaah!)我还没完蛋!(I wasn’t finished yet!)25年来,我一直没有吃完比赛,这一直困扰着我.因此,在我曾经拥有的所有计算机杂志中,这是我所保留的杂志.(It has been eating at me for over a quarter-century, that I didn’t finish this game. Of all the computer magazines I ever had, this one was the one I kept, for that reason.) 所以现在我抓到了(黑鲸和白鲸).它已被移植-尽可能接近"完成”.经验得到了回报;它教会了我很多关于游戏设计的知识.(So now I’ve caught the (black and) white whale. It is ported - it’s as close to “done” as it needs to be. And the experience has paid off; it taught me a lot about game design.)
使用代码(Using the code)
使用代码的最简单方法是下载”(The simplest way to use the code is just to download “)TIMPIST.exe.zip(TIMPIST.exe.zip)",将其文件放在某个位置的某个文件夹中,运行可执行文件” .exe”,然后玩得开心.(", put its files into some folder somewhere, run the executable “.exe” and have fun. Yay!)
哦,等等,我们在(Oh, wait, we are on the)**码(Code)**项目.应该涉及代码.哦,(Project. There is supposed to be code involved. Oh,)行(all right). (Killjoys.)(. (Killjoys.))
我是使用Visual Studio 2012编写的.该解决方案可能针对.NET 4.5,但项目针对.NET 2.0.因此,如果您使用的是早期的Visual Studio,则可能要创建一个新的解决方案,并在此处附加三个vbproj.(I wrote this with Visual Studio 2012. The solution might target .NET 4.5, but the projects target .NET 2.0. So, if you are on an earlier Visual Studio, you might want to create a new solution and attach the three vbproj’s here thereto.)
至于此解决方案中的三个项目.一个是游戏本身,这完全归功于James W Wood,因为在这里我只是他的(As to the three projects in this solution. One is the game itself, which is credited entirely to James W Wood, since here I was just his) 猴子代码(code-monkey) .另一个是音乐库,那只猴子是我们自己的(. Another is the music library, which monkey was our very own) 科林安格斯
麦凯(Colin Angus Mackay) .第三是我的贡献,它是Extended-BASIC" DRAW"的解析器.我将从我自己的东西开始.(. The third is my contribution, which is a parser for the Extended-BASIC “DRAW”. I’ll start with my own stuff first.)
画 “"(DRAW “")
彩色计算机缺少用于今天的街机时间图形的RAM.取而代之的是在其"扩展的BASIC"库中提供的16K +版本,用于绘制简单的小精灵.这意味着是DRAW命令.该命令将解析字符串并跟踪笔的状态.书呆子时代将立即认识到(The Color Computer lacked the RAM for arcade-time graphics of the detail of today. The 16K+ versions supplied instead, in its “Extended BASIC” library, a means to draw up simple small sprites. This means was the DRAW command. The command would parse a string and keep track of the pen’s state. Nerds Of Age will immediately recognise the) 乌龟系统(Turtle system) ,如果不是来自CoCo,则至少来自LOGO.(, if not from CoCo then at least from LOGO.)
如今,不再需要运行时拼写,因为我们可以在Paint中绘制图形并为这些坏男孩加屏盖.但我们(Nowadays, runtime spriting isn’t much needed, since we can just draw the graphics in Paint and screencap those bad boys. But we)**怀旧迷(nostalgia buffs)**将需要它…(are going to need it …)
因此,如果您不拥有DeLorean,请首先将CoCoDrawParser项目与您的解决方案连接.这可以通过"添加参考"来完成.(So, in case you don’t own a DeLorean, first connect the CoCoDrawParser project in with your solution. This can be done by “Add Reference”.)
要设置状态,请实例化一个(To set up the state, instantiate a) TurtleDrawer
.它必须钩入一个(. It must be hooked into a) Graphics
目的.我还要求使用前景Pen对象.在此示例中,该笔在整个游戏中将是相同的,因此它是一个const(嗯,只读静态).(object. I am also demanding a foreground Pen object. In this example, that pen is going to be the same throughout the game, so it’s a const (well, readonly static).)
private static readonly Pen PEN_FORE = new Pen(new SolidBrush(Color.White));
...
CocoDrawParser.TurtleDrawer td = new TurtleDrawer(thisGraphics, PEN_FORE);
然后,绘制字符串.下面的代码将提起笔,将笔设置为(30,30),放下笔,将比例缩放为” 4”,然后从右向下-左上绘制.那是一个矩形.(Then, draw the string. The following code will lift the pen, set the pen to (30,30), put down the pen, scale to “4”, and draw right-down-left-up. That’s a rectangle.)
td.Draw("BM30,30S4R195D135L195U135");
对于已解析的命令,我所包含的内容只不过是(For the parsed commands I have included not much more than what is necessary for)**这个(this)**游戏.如果我在Sha’llah中将更多游戏移植到.NET上,其余的将由我填写.或者,其中一个可以做到!(game. If, in sha’llah, I port more games over to .NET, I’ll fill the rest in. Or, one of you can do it!) 要进入解析器本身,我使用(To get into the parser itself, I use a) 策略模式(Strategy pattern) (带有((with hints of) 州(State) ).我将每个命令保持在自己的功能中.首先,我定义标准函数格式,该格式始终采用字符串输入.并且我定义了要委托的字符的哈希表:(). I keep each command in its own function. First I define the standard function format, which always takes a string input; and I define a hashtable of characters-to-delegates:)
private delegate void DrawFunc(string s);
private Dictionary<char,DrawFunc> _parser;
在实例化时,我为相关函数分配了字符:(At instantiation, I assign characters to the function in question:)
_parser = new Dictionary<char,DrawFunc>()
{
{'M', new DrawFunc(Move)},
...
}
的(The) Draw
命令接受必须解析的字符串.我遍历该字符串.一旦弄清楚接下来要执行的命令,便得到其参数并调用其功能:(command takes in the string which must be parsed. I loop through that string. Once I figure out what command is next, I get its argument and I invoke its function:)
_parser[command](strarg);
[因此,您问-为什么不只使用([So, you ask - why not just use the) switch()
C和VB语言中正确的语句?我的回答是我…不喜欢(statement that is right there in the C and VB languages? My answer is that I … don’t like) switch()
.特别是在这里,当很大一部分代码围绕着将功能分配给条件时.策略模式强制编码人员编写自文档功能.我们可以立即看到将有一个" Move"命令,并且可以在_parser中找到其代码.](. Especially here when such a large part of the code revolves around assigning the function to a condition. The Strategy Pattern forces the coder to write self-documenting functions. We can see right away that there is going to be a “Move” command and we can go find its code in the _parser.])
穿线(Threading)
人们很快发现CoCo在面向对象乃至结构化编程方面并没有提供太多帮助.但是,人们也可以发现像这样的动作游戏之间的共同点.(One finds out pretty quickly that the CoCo didn’t offer much in the way of object-oriented or even structured programming. But one can also uncover general points of commonality amongst action games like this one.)
任何动作游戏都有两个竞争对手:玩家和敌人.玩家根据自己的时间行事,而敌人则以(Any action game has two competitors: the player, and the enemy. The player acts on his own time and the enemy, famously,) 行动(acts on)**他的(his)**自己的时间(own time) . . .(. . .)
类似"暴风雨"的类型稍微简单一点,因为敌人就是时钟.当玩家面对那个"敌人"时,它就是"用按钮射击!“在正确的地方;然后该操作将重置时钟.这使我们解放了,不必太在乎敌人的线程.(The Tempest-like genre is a little simpler in that the enemy is simply the clock. When the player confronts that “enemy”, it is just “Fire With BUTTON !!” at the right place; and that action resets the clock. That frees us up not to care too much about the enemy’s thread.)
同时,在如此小巧的游戏中,不要过度设计也很重要.我的意思是,出于天国的缘故,原始内容以两页的条目发布在免费的新闻通讯中.(At the same time it is important, in a game of this tiny size, not to over-design. I mean the original thing was posted as a two-page entry in a free newsletter for heavens' sake.)
应对敌人-“武器”-(To handle the enemy - the “weapon” - a) Timer_Tick
事件将在设定的时间触发,并增加一个计数器.当计数器达到最大值(此处为5)时,游戏将通过LoseLife()例程惩罚玩家.(event will fire at set times, and increment a counter. When the counter gets to the maximum (5, here) the game punishes the player by the LoseLife() routine.)
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();
}
}
要处理播放器的动作,最好为播放器的控件创建侦听器-或者更好的是,(To handle the player’s action, it is best to create listeners for the player’s control - or, better, to)**找(find)**这些监听器已经实现-并响应播放器的事件.播放器的字段是图片框. Picturebox高兴地带有它自己的(those listeners already implemented - and to react to the player’s events. The player’s field is the picturebox. The picturebox happily comes with its very own) MouseMove
和(and) MouseClick
事件.(events.)
请注意,当线程分开时,即使只有两个线程在此处运行,我们也无法再假定线程将同步.因此,如果游戏做的耗时的事情不属于游戏玩法,例如(Note that when the threads are separated, even here where only two threads run, we can no longer assume that the threads will be in sync. So if the game is doing something time-consuming that is not part of gameplay - like the explosion sequence in) LoseLife()
-游戏在进行期间应停止计时器.(- the game should stop the timer while that is going on.)
更阴险的是,当玩家仍在移动鼠标时,敌人可能已经赢了.我发现从播放器的角度刷新屏幕时会发生这种冲突.因此,在播放器的事件处理程序中,我将代码放入(More insidiously, the enemy might have won already when the player is still moving the mouse. I found that such conflict occurred when refreshing the screen from the player perspective. So, in the player’s event handlers, I put in code to)**检查(check)**在刷新屏幕之前,敌人没有赢得胜利.(that the enemy hadn’t won before I refreshed the screen.)
这就是(This is how) MouseMove
被实施.我将玩家限制在场地边缘.如果玩家的鼠标位于棋盘的右侧,则我接受该决定.(is implemented. I restrict the player to the rim of the field. If the player’s mouse is in the right part of the board then I accept that decision into the) _player
目的.(object.)
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();
}
}
}
这就是(And this is how the) MouseClick
被处理. (记住” AWinnerIsYou";我将在双缓冲位中再次提到它.)(is handled. (Remember “AWinnerIsYou”; I’m going to bring that up again in the double-buffering bit.))
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();
}
双缓冲(The double buffer)
如果您的游戏板只是黑屏,则双缓冲无关紧要.但是谁想在黑屏上播放呢?如果您的背景不应该发生变化,并且周围有动静的部分,那么您将需要:(If your playing board is just the black screen, double-buffering does not matter. But who wants to play on a black screen? If you are on a background that is not supposed to change, and there are moving parts about, you are going to want:)
- 刷新屏幕并(to refresh the screen and)
- 不要处理闪烁和重画所有内容.(not to be dealing with flickering and redrawing everything.)
那是古老的地方(That is where the venerable) 双缓冲区模式(double buffer pattern) 在彩色计算机中,这是通过分页完成的,游戏将通过PMODE在页面之间切换.在.NET 2.0中,我们捕获图形的矩形并将其存储在(comes in. In the Color Computer, this was done by Paging, and the game would flip from page to page by PMODE. In .NET 2.0, we capture a rectangle of graphic and store it in)
BufferedGraphics
.(.)
private BufferedGraphics _backBuffer;
这个概念是您将瞬息万变的东西(包括您和敌人)分离出来,并将背景素材放入缓冲区.(The concept is that you separate out the stuff that changes moment by moment - you, and the enemy - and you put the background stuff in a buffer.)
_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;
}
当您和/或敌人移动时,游戏会像我在游戏中一样在您的旧位置上设置缓冲区(When you and/or the enemy makes a move, the game imposes the buffer on your old position(s), as I do in) RefreshBoard()
:(:)
_backBuffer.Render(_g);
并重新绘制运动部件.(and redraws the moving parts.) 后台缓冲区也可以更改,但是预计不会经常更改.因此,当您需要编写分数和您的生活时,它们就会被写入该缓冲区.这是增加分数的示例" AWinnerIsYou":(The back-buffer can change too, but it’s expected not to change as often. So your score and your lives will be written to that buffer when they need to be written. Here is the increase-score example, “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();
}
并且,当稍后重新添加缓冲区时,该新信息将在那里.(And when the buffer is re-imposed later, that new information will be there.)
兴趣点(Points of Interest)
令我印象深刻的是双缓冲的概念这么早就为人所知. PMODE不是没有(I was impressed that the concept of double-buffering was known so early. PMODE ain’t got no)**时间(time)**闪烁!(to flicker!) 我最大的麻烦不是写DRAW解析器.实际上,这有点有趣.(The biggest headache I had wasn’t in writing a DRAW parser. That was somewhat fun actually.) 最大的头痛是(The biggest headache was)声音(sound).扩展BASIC只是让您以字符串形式演奏一堆音符.没有本机.NET的方法.我找不到真正的.NET方法来简单地访问声音-仅以快速字符串的形式播放" note A,note B"并完成它(例如在Extended BASIC中).我到处看.然后我说"要接受",然后从四堂课中剔除(. Extended BASIC just let you play a bunch of notes in a string. There is no native .NET way to do that. I could find no real .NET way of simply accessing the sound - of just PLAY’ing “note A, note B” in a quick string and being done with it (like in Extended BASIC). I looked everywhere. Then I said “to heck with it” and just ripped off four classes out of) Mackay的.NET 1" Beep"项目(Mackay’s .NET 1 “Beep” project) 我希望他不要起诉.(and I do hope that he does not sue.)
历史(History)
- 2012年12月14日:发布.(14 December 2012: Posted.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C# Windows .NET .NET2.0 GDI+ Dev 新闻 翻译