Dungeon of Despair:一个使用 Conscript 的脚本化演示游戏






4.96/5 (52投票s)
本文介绍了一个使用Conscript的脚本驱动演示游戏,Conscript是一个在早期文章中介绍过的脚本引擎。
文中插入的截图来自Lockdown [Alex Varanese, 2002, Game Scripting Mastery]。
引言
本文介绍了“绝望地牢”(DOD),一个完整的游戏应用程序,旨在演示在早期文章中介绍的Conscript脚本引擎。该游戏使用GDI+渲染图形,并作为一个无边框的Windows应用程序进行开发。
灵感
DOD的灵感来源于Lockdown,一款由Alex Varanese在其著作Game Scripting Mastery中为Xtremescript脚本引擎开发的演示游戏。Lockdown的截图已在上述截图上方嵌入。在Lockdown中,你是一名被困在某个空间站的机器人,必须找到四把颜色编码的钥匙,并将它们放入中央控制室以解锁空间站并逃生。玩家一次只能看到空间站的一个房间,每个房间通过门通往一个或多个房间。保护钥匙和控制室的是具有不同智能水平的敌方机器人,其中一些不仅会在接触时伤害玩家,甚至在某些情况下会向玩家开火。玩家可以反击,并且可以休息以恢复机器人的生命值。如果玩家失去所有生命值或空间站被解锁,游戏结束。Lockdown通过实现环境效果(如闪烁的灯光)和敌方机器人AI作为脚本来展示脚本的应用。
DOD的游戏机制与Lockdown非常相似,但背景设定灵感来源于经典的2D RPG游戏。在DOD中,玩家是一名被困在魔法地牢中的法师,只能通过激活魔法传送门才能逃脱。而传送门只有在找到四瓶元素之瓶并将其放置在传送门上才能激活。然而,玩家必须面对许多敌人,敌人的类型决定了其整体智能水平和威胁等级。玩家在接触敌人或被敌方法术击中时会失去生命值。生命值显示在屏幕左上角的彩色生命值计量表中。屏幕右上角的四个图标代表玩家收集的元素之瓶的库存。
脚本的应用
与Lockdown一样,DOD也是一个足够复杂的游戏应用程序,值得使用脚本,但其范围仍然局限于说明性示例。DOD主要使用Conscript脚本来实现敌方AI。没有专门用于环境效果的脚本。但是,地牢的布局及其内容是由脚本定义的复杂数据结构来确定的。游戏还演示了一个简单的脚本驱动的过场动画系统,通过进入每个房间会触发一个可选的过场动画脚本函数,以帮助玩家沉浸在游戏中。
美术
背景图是通过处理大量的(经过大量)照片生成的。角色精灵图和对话面部集合是使用在线RPG精灵图生成器生成的,可以在这里找到。
游戏说明
要运行游戏,请编译附加的解决方案并执行生成的二进制文件。执行后,游戏应用程序会显示一个组合标题/菜单屏幕,用户可以在其中选择开始游戏会话或退出应用程序。菜单项选择可以通过光标键和空格键或回车键执行。一旦开始游戏会话,用户可以使用光标键移动玩家,并通过空格键发射法术。在过场动画期间,玩家控制将暂时禁用。用户可以通过按Esc键随时退出游戏会话并返回标题屏幕。游戏结束屏幕可以通过按空格键或回车键退出。
背景
嵌入式脚本引擎已被证明是游戏制作中的宝贵工具。它们使游戏设计师能够在不干扰游戏程序员的情况下构建游戏内容,而程序员可以专注于游戏开发的其它更技术性的方面。另一个好处是,生产周转时间大大缩短,因为设计师可以在不重新构建游戏应用程序的情况下更改游戏内容。脚本引擎也是“可修改”游戏的基石,这些游戏鼓励社区通过用户创建的内容来延长游戏的生命周期。
实现
游戏应用程序遵循面向对象的(OO)设计范式,其中游戏的主要组件由相互交互的对象表示。该应用程序使用无边框的主窗口,使用GDI+渲染图形,并通过键盘事件读取玩家输入。游戏的主要状态——即标题屏幕、游戏会话、游戏结束屏幕等——是通过继承自通用GameState
类的游戏状态类来实现的。应用程序维护一个当前活动状态类的引用,该类负责处理自身的行为。每个状态还可以请求转换为新状态,例如从标题屏幕开始新游戏或结束游戏会话并显示游戏结束屏幕。
地牢表示
游戏世界由一个Dungeon
类表示,它由一系列相互连接的房间组成,每个房间由一个Room
类表示。每个房间通过一个Doors
类实例链接到一个或多个其他房间,该实例维护四个主要方向的房间链接。附加的房间属性提供了装饰性特征,以增加房间的多样性,例如房间周围的闪烁火把或一个圆形基座,通常放置在交叉房间中。其中一个房间包含元素传送门,这由Dungeon
类中的一个房间引用表示。类似引用也用于起始房间以及四个元素之瓶在地牢中的放置位置。
地牢数据在Conscript脚本Dungeon.cns
中定义,它是一个多级关联数组,其结构类似于Dungeon
、Room
和Doors
实例的层次结构。该脚本在切换到游戏会话状态时加载并执行。结果结构从脚本的变量字典中提取,并进行处理以构建地牢布局。以下是地牢布局脚本的摘录。
var s_dungeon;
function main()
{
var rooms = {};
// Room 00
rooms.R00 = {};
rooms.R00.Pedestal = true;
rooms.R00.Doors = {};
rooms.R00.Doors.East = "R10";
rooms.R00.Doors.South = "R01";
rooms.R00.TorchNE = true;
rooms.R00.TorchSW = true;
rooms.R00.Enemies = {};
rooms.R00.Enemies.Enemy0 = {};
rooms.R00.Enemies.Enemy0.Type = "Wizard";
rooms.R00.Enemies.Enemy0.Quantity = 1;
rooms.R00.Enemies.Enemy1 = {};
rooms.R00.Enemies.Enemy1.Type = "Pig";
rooms.R00.Enemies.Enemy1.Quantity = 1;
rooms.R00.Enemies.Enemy2 = {};
rooms.R00.Enemies.Enemy2.Type = "Lizard";
rooms.R00.Enemies.Enemy2.Quantity = 1;
// Room 10
rooms.R10 = {};
rooms.R10.Doors = {};
rooms.R10.Doors.West = "R00";
rooms.R10.Doors.East = "R20";
rooms.R10.TorchNE = true;
rooms.R10.TorchSE = true;
rooms.R10.Enemies = {};
rooms.R10.Enemies.Enemy0 = {};
rooms.R10.Enemies.Enemy0.Type = "Wizard";
rooms.R10.Enemies.Enemy0.Quantity = 1;
rooms.R10.Enemies.Enemy1 = {};
rooms.R10.Enemies.Enemy1.Type = "Pig";
rooms.R10.Enemies.Enemy1.Quantity = 2;
// more rooms here..
// :
// :
// dungeon top-level array
s_dungeon = {};
// uncomment for infinite health
//s_dungeon.Cheat = true;
s_dungeon.Rooms = rooms;
// define starting room and portal room
s_dungeon.StartingRoom = "R34";
s_dungeon.PortalRoom = "R30";
// define vial rooms
s_dungeon.RedVialRoom = "R20";
s_dungeon.GreenVialRoom = "R40";
s_dungeon.YellowVialRoom = "R44";
s_dungeon.BlueVialRoom = "R24";
}
实体
玩家、敌人和弹体都继承自一个通用的Entity
类,该类提供诸如放置和移动之类的基本服务。玩家由一个Player
类表示,其中包含玩家生命值以及拥有元素之瓶的标志。每个敌方实体由一个Enemy
类表示,该类包含敌方生命值、一个引用了适当AI脚本的ScriptContext
实例以及一个用于敌人移动速率的速度属性。
每个敌人脚本都设计为与应用程序的其余部分并行执行,因此包含一个主循环,在其中感知环境并采取适当的行动。为了驱动与脚本上下文相关联的敌人实例,注册了许多宿主函数,并将敌人实例分配为函数处理程序。以下是敌方AI宿主函数的列表。
int GetRandom(int iMin, int iMax)
- 返回给定范围内的随机整数
Move(int iX, int iY)
- 将敌人移动到给定坐标
SetDirection(int iDirection)
- 将敌人朝向给定方向
SetAutomaticDirection(bool bFlag)
- 根据速度启用/禁用自动方向
{"X":int, "Y":int} GetPosition()
- 获取敌人的坐标
{"X":int, "Y":int} GetPlayerPosition()
- 获取玩家的坐标
CastSpell()
- 以敌人面向的方向施放法术
过场动画
每当玩家进入一个房间时,游戏应用程序会检查是否有过场动画可用,如果有,则暂时从用户那里收回玩家的控制权,同时播放过场动画。过场动画实现为脚本文件中定义的脚本函数Cutscenes.cns
。应用程序假设函数的名称格式为Cutscene_RNN
,其中RNN是房间标识符,并据此定位函数。过场动画函数由一系列宿主函数调用组成,用于控制场景中的角色交互。以下脚本函数定义了中心房间R32
的过场动画。这个特别的场景鼓励玩家探索北边的出口。
// centre cut-scene
function Cutscene_R32()
{
// do scene only first time round
if (s_canSeeLight != null) return;
s_canSeeLight = true;
// disable player control
Player_SetActive(false);
// move uo to room centre and talk
Player_MoveTo(320, 160);
while(Player_Moving()) yield;
Player_Say("I can see strange lights coming beyond this doorway!");
while(TextWindow_IsActive()) yield;
// move closer to door and talk some more
Player_MoveTo(320, 170);
Player_Say("Maybe I should have a look...");
// return control to player
Player_SetActive(true);
}
为了支持脚本化的过场动画,游戏应用程序定义了以下宿主函数:
Player_SetActive(bool bFlag)
- 启用/禁用玩家控制
Player_SetPosition(int iX, int iY)
- 设置玩家的位置
Player_MoveTo(int iX, int iY)
- 请求玩家移动到指定位置
bool Player_Moving()
- 如果玩家正在移动,则返回true
Player_Say(String strMessage)
- 让玩家在聊天窗口中说出某句话
bool Player_HasAllVials()
- 如果玩家收集齐了所有四瓶元素之瓶,则返回true
Boss_SetActive(bool bFlag)
- 显示/隐藏敌方首领
Boss_SetPosition(int iX, int iY)
- 设置首领的位置
Boss_MoveTo(int iX, int iY)
- 请求敌方首领移动到指定位置
bool Boss_Moving()
- 如果敌方首领正在移动,则返回true
Boss_Say(String strMessage)
- 让首领在聊天窗口中说出某句话
Boss_SpawnEnemy(String strType)
- 生成指定类型的敌人
bool TextWindow_IsActive()
- 如果聊天窗口仍处于打开状态,则返回true
关注点
从概念上讲,DOD是一个非常简单的游戏,本可以轻松地在不使用嵌入式脚本的情况下开发。然而,得益于脚本,不仅可以改变地牢布局,还可以改变敌人的AI行为和游戏整体的情节线。将更多权力交给脚本系统会增加游戏的“可修改性”。在下一篇文章中,我将介绍一个完整的Conscript IDE应用程序,它将以更复杂的方式演示Conscript API的使用,例如提供调试功能和实现宿主函数模块插件机制。
相关文章
- EezeeScript:用于 .NET 的简单可嵌入脚本语言
- Conscript:一个可嵌入的、用于.NET的编译型脚本语言
- Conscript IDE:一个为Conscript脚本语言实现的集成开发环境(IDE)。
历史
- 2007年7月20日 - 发布第一个版本