另一个星际迷航游戏(复古版)
重构的星际迷航游戏,采用 2D 复古风格
引言
在阅读了 Michael Birken 的文章后,不得不承认,文章再次证明了一个好游戏只有一个要素:玩法。而不是画面,也不是声音。作为 Introversion Software 游戏(它们都有特殊的“老旧”外观)的忠实粉丝,我一直在想,为什么不为这款游戏添加这种简单的复古风格呢。
正如 James Curran 所说,Michael Birken 的文章从头到尾都涵盖了这个游戏,我很难再补充什么。因此,我将更侧重于构建一个看起来像主机游戏的 2D 游戏(而不是主机本身)的区别。
背景
我需要解决的第一个问题是,将使用哪种技术来实现游戏。DirectX 还是 OpenGL?第二个问题更重要:由于这是我的第一个图形项目,我能否掌握所选技术,并及时编写所有必要的代码?
在网上搜索了一段时间后,我偶然发现了以下网站:IrrLicht。这是一个免费的图形引擎,支持 DirectX 和 OpenGL,这也解决了我的第一个问题。这个引擎同时支持 2D 和 3D 模式,这非常完美。我可以从 2D 开始,然后过渡到 3D,而无需更换引擎。
不使用游戏机的一个主要区别在于我们处理输入的方式。游戏机程序是输入驱动的程序。在分析输入后,我们执行所需的逻辑,然后重绘屏幕。Windows(非游戏机)程序是事件驱动的程序。这意味着我需要分析事件并创建自己的输入来驱动游戏逻辑。而且,因为这是一个窗口,我需要自己绘制屏幕,并在等待输入的同时不断更新屏幕。
使用代码之前
如果您想使用和编译此代码,还需要 IrrLicht SDK,可以 在此处 下载。它包含了您开始所需的一切,包括一个已构建的 DLL 和 lib。
将 SDK 放置在磁盘上后,您需要将包含文件和库文件的位置添加到 Visual Studio。在 工具 菜单下打开 选项 菜单。选择 项目和解决方案 选项,然后选择子选项 VC++ 目录。
Using the Code
您现在可以开始使用和编译此代码了。我从一个控制台项目模板开始;这样做的好处是控制台将用作输出和跟踪窗口,从而更易于调试引擎。在 main 函数中,我创建了一个 Game
对象。该对象包含设置、运行和结束游戏的各项功能。
int _tmain(int argc, _TCHAR* argv[])
{
Game* pTheGame = Game::getInstance();
if(pTheGame)
{
pTheGame->setupGame();
pTheGame->createData();
pTheGame->runGame();
pTheGame->endGame();
}
return 0;
}
setupGame()
方法创建游戏窗口和游戏动作。游戏动作将负责游戏逻辑。我将游戏逻辑放入了一个 Act
对象中。这个 Act
对象被交给一个 Director
对象。而这个 Director
对象将允许您在不同的动作之间切换。这款游戏有三个动作:IntroAct
、PlayAct
和 CreditsAct
。Intro 将绘制星际迷航标志并展示游戏目标。Play 将包含实际游戏,而 Credits 将在游戏结束后被调用,以纪念我们胜利的舰长,或者为最伟大星舰的毁灭而哭泣。我们需要最后一个对象是 InputManager
。这个对象将把 IrrLicht 引擎返回的事件转换为我们希望接收通知的输入。在我们的例子中,这些将是 KeyPress
事件。
为了将 KeyPress
事件发送到各个动作,我本来可以使用一个简单的回调函数,但我喜欢 C# 的委托概念。在 C++ 中,这可以通过 Functor 来完成。 boost 库 提供了几个类来执行此操作,但我不想使用 boost(至少在这篇文章中不想)。因此,我创建了自己的硬编码 **Functor** 来完成这项工作。
struct FKeyPressed
{
virtual ~FKeyPressed() {};
virtual bool operator()(EKEY_CODE) = 0;
};
template class KeyPressed : public FKeyPressed
{
public:
typedef bool (ACTOR::*FunctionType)(EKEY_CODE);
public:
KeyPressed(ACTOR* pActor, FunctionType pFunctor)
{
m_pActor = pActor;
m_pFunctor = pFunctor;
}
virtual ~KeyPressed() {};
virtual bool operator()(EKEY_CODE keyCode)
{
return (m_pActor->*m_pFunctor)(keyCode);
}
protected:
ACTOR* m_pActor;
FunctionType m_pFunctor;
};
希望接收 KeyPress
事件的 Act
对象只需要提供一个具有以下签名的函数
bool OnKeyPressed(EKEY_CODE keyCode);
在 runGame
函数中,屏幕被渲染。对我们来说,这意味着将调用活动 Act
的 draw
函数。我们可以在这里绘制所有内容(对于 IntroAct
和 CreditsAct
,由于内容不多,我将这样做),或者我们将一些 IDrawable
对象加载到 Act
中。
我使用了两种类型的 IDrawable
:一种是相对静态的,另一种则更动态。静态可绘制对象的例子是 ShipDisplay
。此类在屏幕右侧显示飞船和游戏的状态。此信息始终需要绘制。动态可绘制对象的例子是 Torpedo
,它将显示一段时间然后从屏幕上移除。这种类型,我称之为 Animator
,实现了 IAnimator
接口,该接口扩展了 IDrawable
接口。例如 Phaser
类。Animator
提供了所有绘制代码和生命周期控制代码。因此,Phaser
只需要添加不同的部分。
void Phaser::updateInfo(Info& rInfo)
{
if(rInfo.Alpha >= 0)
{
rInfo.Alpha += rInfo.Fade;
}
if(rInfo.Alpha >= 255)
{
rInfo.Alpha = 255;
rInfo.Fade = - rInfo.Fade;
}
}
当 Phaser
的生命周期结束时,会调用 endAnimator
。在 Phaser
的情况下,目标飞船被击中。
void Phaser::endAnimator()
{
if (m_pVessel)
{
m_pVessel->hitPhaser(m_iEnergy);
}
}
最困难的部分之一是实现控制台界面。Console
类负责外观和输入。每当按下 Enter 或 Escape 键时,就会触发 CommandManager
。该管理器存储游戏输入的当前状态。
enum Mode
{
WaitForCommand,
NavigateWaitForCourse,
NavigateWaitForDistance,
LaunchWaitForEnergy,
LaunchWaitForHit,
LaunchWaitForCourse,
LaunchWaitForCoordinates,
TransferWaitForEnergy,
ComputerWaitForCommand,
WaitForAnimation
};
CommandManager
验证输入并采取适当的操作,例如,让 Enterprise
对象向护盾传输能量。第三个类是 Controller
,该对象实际创建 Torpedo
或 Phaser
动画程序并将它们添加到 PlayAct
。runEnemyAI
函数检查是否有运行的需要,以及该区域内是否有敌舰。有三个飞船:Enterprise
、KlingonShip
和 StarBase
,它们都实现了 IVessel
接口。
关注点
首先,我想为使用像 IrrLicht 这样优秀的库来仅绘制一个简单的控制台外观而道歉。但是,将字符 <E> 更改为企业号的 2D 图像非常容易。或者,您可以做得更进一步,将其制成完整的 3D。修改 Controller
和 CommandManager
类以使其成为实时而非回合制也应该很容易。
参考文献
- 将我引向撰写第一篇文章的文章:Michael Birken 的 Star Trek 1971 Text Game
- 来自 Mike Mayfield 的原始代码,我从中大胆地复制了帮助。
历史
- 2008 年 8 月 27 日 - 本文首次发布