(笔记)《游戏脚本高级编程》——第2章 脚本编程系统的应用(上)

本章内容:

  • 介绍脚本系统是如何用于解决下列问题的:
  • RPG游戏中与内容相关的部分——非玩家角色和场景细节
  • RPG游戏中的物品、武器和敌人
  • 第一人称射击游戏中的物品、谜题和机关
  • 第一人称射击游戏中敌人的行为

1.游戏引擎和内容是完全分离的,所以当游戏玩家购买游戏软件的时候,实际上购买了两个部分:一个编译过的游戏引擎和一系列可以对游戏功能进行扩展的脚本。这种结构称之为模块化结构。这种方法一种常见的应用是以“情节(episode)”的形式发布游戏。这就意味着游戏软件店在在销售游戏软件的时候只出售一部分游戏内容以及可以在上面运行游戏的游戏引擎。当游戏玩家完成了第一个游戏情节的时候,他们就可以以较少的费用下载或者购买另外的如patches和add-ons样式的游戏情节(这不就类似于DLC吗)。


2.角色扮演游戏(Role Playing Games,RPGs)

RPG游戏一般要比其他类型的游戏要使用更多的脚本,原因就在于RPG游戏的确适宜于使用脚本,它们确确实实需要大量的游戏内容。自然而然,RPG的游戏开发人员就需要一个很好的方法以一种结构化的、有组织的方式来开发这些内容。

下面将分析一下RPG游戏的传统内容,以便理解为什么脚本对于设计角色扮演的游戏来说那么适用。

①复杂而又有深度的故事情节:

RPG游戏看起来更像是一部交互式小说,这就意味着游戏中将会出现进行无穷行对话的人物角色和带有无数“场景点”的高度结构化的游戏场景。而在玩家冒险经历的每一个场景点,游戏都需要玩家在到达该处之前完成了哪些主要的事情以确定游戏世界中的现在状态以及将要发生的事情。解决的方法就是采用一组“标记(flags)”来记录场景点或者是游戏世界中的现在状态。每个标记代表游戏中的一个事件,可以为TRUE或者FALSE(当然可以不是布尔类型)。

这套系统的实现方法有很多种。一种就是在引擎的源代码中直接建立标技术组,并向游戏脚本提供一个访问接口,这个接口允许脚本对于数组进行读写操作。这种方法将所有逻辑和功能都写在了脚本上,脚本只需通过接口访问数组就可以完成这套系统。第二种方法就是如果脚本系统能力很强的话,甚至可以将数组从引擎中剥离,直接将数组存储到脚本内部并访问,这种方法是最理想的,因为这让游戏逻辑和引擎进行了分离。不过前者也是可用的。

②非玩家角色(Non-Player Characters, NPCs)

NPC在游戏里也是至关重要的,他们可以为主角进行剧情的引导,或是和他们交换物品。因此,条件逻辑、循环以及读取游戏flags的能力等就会变得很关键。解决方法如下:

一些简单的对话,只需在NPC和脚本之间建立一些一对一的对应关系。当有NPC问一些带有选项的问题时,系统会根据玩家应答的结果的不同而采用不同的对话策略。或者其他的一些不同的情况,使得NPC变得十分逼真和灵活,这就需要一种更加强大的描述语言来描述他们的行为。

这种语言所需要的特征:

(1)应该具备基本的对话能力

(2)在对话中可以通知游戏引擎在什么时候加入动画

(3)能够查看玩家的物品清单

(4)能够提供一组定做的答案

(5)记录玩家和这个角色的交往历史。此外,理论上游戏玩家在这些单独的对话期间可能会退出游戏然后又重新进入,那么就不仅需要在游戏期间在内存上保留信息,在不同的游戏期间还要把它们保存到磁盘上。即无限期地保存这个NPC相关的变化信息

(6)能够改变flags

下面有段对话:

(游戏玩家第一次和NPC进行对话)

NPC:"Hey, you look familiar."(斜着眼睛盯着玩家的脸)

Player:"Do I?I don't believe we've met."

NPC:"Wait a sec- you're the guy who's gonna save the world from the vampires,right?"

NPC:(如果游戏玩家说的是Yes)"I knew it!Here, take this garlic!"(将大蒜交给游戏玩家)

Player:"Thanks!"

(游戏玩家再次和NPC进行对话)

NPC:"Sorry, I don't have any more garlic.I gave you all I had last time we spoke."

Player:"Well that sucks."(跺脚)

(游戏玩家第三次和非玩家角色进行对话)

NPC:"Dude I told you, I gave you all my garlic.Leave me alone!"

Player:"But I ran out, and there's still like 10 more vampires that need to be valiantly defeated!"

NPC:"Hmm...well, my brother lives in the next town over, and he owns a garlic processing plant.I'll tell him you're in the area,and to batch ready for you.Next time you're there,just talk to him, and he'll give you all the garlic you need."

Player:"Thanks, mysterious garlic-dispensing stranger!"

NPC:"My name's Gary."

Player:"Whatever."

(游戏玩家和NPC进行了三次以上的对话)

NPC:"So,have you seen my brother yet?"

这段对话可以用以下类似于C/C++的脚本代码编写:

static int iConverseCount = 0;
static bool bIsPlayerHero = FALSE;
main()
{
    string strAnswer;
    if(iConverseCount == 0)
    {
        NPCTalk("Hey, you look familiar.");
	PlayAnim(NPC, SQUINT);
	PlayerTalk("Do I?I don't believe we've met.");
	strAnswer = NPCAsk("Wait a sec- you're the guy who's gonna save the world from the vampires,right?", "Yes", "No");
	if(iAnswer == "Yes")
	{
	    NPCTalk("I knew it!Here, take this garlic!");
	    GiveItem(GARLIC, 4);
	    PlayerTalk("Thanks!");
	    bIsPlayerHero = TRUE;
	}
	else
	{
	    NPCTalk("Ah.My mistake.");
	    bIsPlayerHero = FALSE;
	{
    }
    else
    {
	if(bIsPlayerHero)
	{
	    if(iConverseCount == 1)
	    {
		NPCTalk("Sorry, I don't have any more garlic.I gave you all I had last time we spoke.");
		PlayerTalk("Well that sucks.");
		PlayAnim(PLAYER, STAMP_FEET);
	    }
	    else if(iConverseCount == 2)
	    {
		NPCTalk("Dude I told you, I gave you all my garlic.Leave me alone!");
		PlayerTalk("But I ran out, and there's still like 10 more vampires that need to be valiantly defeated!");
		NPCTalk("Hmm...well, my brother lives in the next town over, and he owns a garlic processing plant.I'll tell him you're in the area,and to batch ready for you.Next time you're there,just talk to him, and he'll give you all the garlic you need.")
		PlayerTalk("Thanks, mysterious garlic-dispensing stranger!");
		NPCTalk("My name's Gary");
		PlayerTalk("Whatever.");
		SetGameFlag(GET_GARLIC_FROM_GARYS_BROTHER);
	    }
	    else
	    {
	        NPCTalk("So,have you seen my brother yet?");
	    }
	}
	else
	{
	    NPCTalk("Hello again.");
	{
    }
    iConverseCount ++;
}

会发现仅仅只是添加了一些新的特征,这个语言就突然变得和C/C++语言相似了。同样的,在编写一个RPG游戏的过程中,数组、指针、动态资源分配等许多更加高级的语言特性也会大放异彩。通常有两种方法来设计脚本:①先设计一个类似于C/C++的语法体系,然后在你需要的时候添加新内容;②同时设计语法和语言的总体结构。前者要比后者容易的多。采用C/C++也会是你的系统更加统一和标准


③物品和武器:

因为很多物品的行为都非常像是宏,可以采用基于指令的语言来编写它们。

物品脚本和游戏脚本一般都需要完成一系列的任务。首先要提到的就是它的后台功能,即这个物品是攻击敌人的,还是治疗友军的,还是打开一道门,还是清理一个过道等等。这个功能只要简单地更改一些代码就可以实现。基于命令的语言可以满足。然而,另一个方面是玩家可以感受到这种武器或者物品的功能的具体表现,比如武器的动画效果等。基于命令的语言就不能满足这个需求了。

解决方法:

最好还是采用那些C/C++样式的、面向对象的语言,因为它们允许物品和武器在脚本内部定义自己的图形效果并且直到最为细节的部分。整个过程很简单,只是涉及到至少要提供一组可供脚本调用的基本的图形程序。所有这些所必须必备的不过就是下面这些常见的东西——制作图形、绘制精灵,也可能是播放电影文件使得玩家可以预览动画效果,通常由DirectX、OpenGL以及SDL所提供的图形API的精选子集组成。

下面举例写一个武器实例:

武器名为Fire Sword。Fire Sword的功能是向敌人发射火球,对于冰系或水系的怪物尤其有效。相反,对火属性的敌人效果较差。同时,由于它的热量,当游戏玩家每次使用这个武器时也会对自身造成一定程度上的伤害。我们就可以通过编写一个火球的动画来完成这个想法。综上,从技术层面来考虑这个武器的功能:

(1)需要能够修改游戏中人物的统计数据,也就是他们的体力值。还要考虑属性的克制时的伤害结算

(2)需要能够真正在屏幕上看到由玩家所在位置到敌人所在位置发射的火球,以及爆炸声。需要处理动画和声音,因此需要条件逻辑和循环。基于命令的语言已经不适用了。此外,主应用程序还应提供一个基本的多媒体API使得脚本最起码能够在屏幕上绘制精灵和播放声音。

(3)还要处理由宝剑发射出来的热量对于自身所造成的的轻微伤害。与(1)类似,仅仅涉及到一些数据的问题,这意味着需要能够访问到玩家的具体状态。

以上任务(2)比较难以解决,其余两个只要基于命令的语言就可解决。这就排除了基于命令的语言而改用C/C++体系的语言。代码如下:

Player.HP -= 4;

int Y = Player.OnScreenY;
for(int X=Player.OnScreenY; X


④敌人:

敌人最为主要的部分在于它同时利用了前面的两个概念:它具有NPC的特征或者是个性为中心的方面,同时它还具有物品和武器的功能以及破坏特性。因此敌人是这两个实体背后的概念相结合的过程。

解决方法:

RPG的打斗场面的主体框架是循环的,再循环过程的每一次迭代过程中,游戏玩家和敌人都要对输入进行分析。当敌人执行相应的打斗脚本时,玩家要准备接受输入的数据;当玩家执行相应的打斗脚本时,敌人要准备接受输入的数据。代码如下:

void Act()
{
    int iWeakestPlayer, iLastAttacker;
    if(iHitPoints < 20)
	if(rand() % 10 == 1)
	    Flee();
    else
    {
	iWeakestPlayer = GetWeakestPlayer();
	if(Player[iWeakestPlayer].iHitPoints<20)
	    Attack(iWeakestPlayer, METEOR_SHOWER);
	else
	{
	    iLastAttacker = GetLastAttacker();
	    switch(Player[iLastAttacker].iType)
	    {
		case NINJA:
		{
		    Attack(iLastAttacker, THROW_FIREBALL);
		    break;
		}
		case MAGE:
		{
		    Attack(iLastAttacker, BROADSWORD);
		    break;
		}
		case WARRIOR:
		{
		    Attack(iLastAttacker, SUMMON_DEMON);
		    break;
		}
	    }
	}
    }    
}
代码解释:首先,敌人脚本会根据它会被打败的可能性有多大。如果这个结果低于一定的临界值(上述代码是低于20个体力值),那么它就会模拟一个试图从打斗现场逃跑的动作。如果在随机1~10之间随到了1,那么就会逃跑。然而,如果他觉着有必要接着继续打斗,那么它就会调用引擎提供的函数来确定哪个是对手中最弱的。如果敌人觉着玩家比较接近被打败的状态(上述代码是玩家体力值低于20),那么就会进行Meteor Shower来将对手打败。如果这个最弱的敌人并不是十分的弱小以至于很容易被打败出局,敌人就会转去袭击那个最后攻击他的人,并会基于对手的类型选择一个具体的进攻策略。

你可能感兴趣的:(游戏编程,游戏编程,《游戏脚本高级编程》)