游戏开发中的人工智能(八):描述式 AI 及描述引擎

版权声明:本文为Jurbo原创文章,转载请加上链接和作者名,标明出处。 https://blog.csdn.net/Jurbo/article/details/75576847

接上文 游戏开发中的人工智能(七):A* 路径寻找算法

本文内容:程序员通常只写描述引擎,而由设计者使用工具创建内容和定义 AI。本章探讨一些开发人员把描述系统应用在游戏中的技巧,以及他们所得到的益处。


描述式 AI 及描述引擎

本章讨论某些技巧,让你把描述系统应用到游戏软件 AI 的问题上,以及这样做以后所能获得的好处。

从最基本的层次上来看,你可以把描述机制想象成非常简单的程序语言,专门为与游戏问题相关的特定工作而量身打造。描述机制可以说是游戏开发过程中,不可缺少的一部分,因为这可以让游戏设计师,而不是游戏程序员,撰写出游戏,并予以精细化。玩家也可以利用描述语言,建立或修改其所处的游戏世界或登记。再进一步的话,你可以在超大型多人在线角色扮演游戏(MMORG)中使用描述系统,当人们实际在玩游戏时,就能改变游戏的行为。

实现描述系统时,可以采用好几种手段。例如,精致的描述系统,可以让实际所用的游戏引擎和现有的描述语言(例如 Lua 或 Python)衔接起来。有些游戏会建立专用的描述语言,专门设计处理个别游戏的需求。虽然有时候利用这些方法比较方便,但是,让游戏分析包含描述命令的标准文字文件,会比较简单。如果采用这种手段,你就可以用任何标准的文字编辑器,建立脚本。在实际游戏中,当游戏开始时或者在某些特定时刻,可以读取脚本,并予以分析。例如,当玩家实际进入城堡时,控制城堡内的生物或事件的脚本,就能被读进来并进行分析。

在游戏软件 AI 的范围里,你可以用描述机制改变对手的属性、行为、响应方式以及游戏事件。本章要解释的就是这些用法。

描述机制技巧

本章我们要建立简单的描述命令,并将之存储在标准文字文件内。我们想避开复杂语言的分析器,就要慎重地选择词汇,才能令人轻松的读写脚本。即,我们所用的字词,要正确地反映脚本要修改的游戏的哪个方面。

描述对手属性

利用某种描述机制,指定每个 AI 对手的所有基本属性。这是很常见也很有益处的做法,这会让我们在开发和测试过程中,能够轻易调整 AI 对手。如果把所有重要数据都直接写进程序里,即使是最基本的修改,也必须得重新编译。

一般而言,我们可以描述对手的属性,比如智能、速率、强度、胆量以及魔法能力。实际上,可以描述的属性类型或数量是没有限制的,真正的决定因素是你正在开发的游戏是什么类型的。例如,有较高智能的 AI,和较低智能的 AI 相比,行为就会不同。较高智能的 AI ,会采用比较高级的路径寻找算法追踪玩家;而较低智能的 AI,则会在试着走向玩家时,很容易被困住。

例8-1 是 设定游戏属性的基本脚本。

//例8-1:设定属性的基本脚本

CREATURE=1;
INTELLIGENCE=20;
STRENGTH=75;
SPEED=50;
END;

此例中,我们的脚本分析器必须编译五个命令。第一个是 CREATURE,指的是要设定哪一个 AI 作为玩家的对手。下面三个分别是 INTELLIGENCE 智力、STRENGTH 强壮、SPEED 速度,都是实际设定的属性。最后的命令 END,是通知脚本分析器,这个生物已经设定完成。

脚本的基本分析

我们已经介绍了基本属性脚本是怎样的,如例8-1。我们打算进一步探索游戏如何读取脚本并予以分析。举个例子,我们要以基本脚本设定巨人的某些属性。建立一个名叫 Troll Settings.txt 的文字文件。例8-3 是巨人设定文件的内容。

//例8-3:设定属性的基本脚本

INTELLIGENCE=20;
STRENGTH=75;
SPEED=50;

例8-3 是一个简单的范例,只为生物设定了3个生物属性。我们要编写一个程序,以便能够轻易地增加其他属性,我们打算编写自己的脚本分析器,使其搜寻指定文件,找出特定的关键字,并返回与该关键字相关的值。例8-4 显示了在实际游戏程序中的内容。

//例8-4:设定属性的基本脚本

intelligence[kTroll] = fi_GetData(Troll Settings.txt,"INTELLIGENCE");
strength[kTroll] = fi_GetData(Troll Settings.txt,"STRENGTH");
speed[kTroll] = fi_GetData(Troll Settings.txt,"SPEED");

在例8-4 中,这三个假想的数组,可以存储生物属性。此时我们通过从名为 Troll Settings.txt 的外部文件中载入生物属性值。fi_GetData( ) 函数会检查这个外部文件,直到找到指定的关键词,然后,返回与该关键字相关的值。这样,游戏设计师可以调整生物设定值,而不需在每次修改之后,都得重新编译程序代码。

接下来详细看一下 从脚本中读取数据,即 fi_GetData( ) 函数,如例8-5 所示。

//例8-5:从脚本读取数据

int fi_GetData(char filename[kStringLength],char searchFor[kStringLength])
{
    FILE *dataStream;
    char inStr[kStringLength];
    char rinStr[kStringLength];
    char value[kStringLength];
    long ivalue;
    int i;
    int j;

    dataStream=fopen(filename,"r");
    if(dataStream != NULL)
    {
        while(!feof(dataStream))
        {
            if(!fgets(rinStr,kStringLength,dataStream))
            {
                fclose(dataStream);
                return 0;
            }
            j=0;
            strcpy(inStr,"");
            for(i=0;i<strlen(rinStr);i++)
            {
                if(rinStr[i]!= ' ')
                {
                    inStr[j]=rinStr[i];
                    inStr[j+1]='\0';
                    j++;
                }
            }
            if(strncmp(searchFor,inStr,strlen(searchFor))==0)
            {
                j=0;
                for(i=strlen(searchFor);i<kStringLength;i++)
                {
                    if(inStr[i]==';')
                        break;
                    value[j]=inStr[i];
                    value[j+1]='\0';
                    j++;
                }
                StringToNumber(value,&ivalue);
                fclose(dataStream);
                return ((int)ivalue);
            }
        }
        fclose(dataStream);
        return 0;
    }
    return 0;
}

例8-5 中的函数一开始是接受两个字符串参数。第一个参数是指定要搜寻的脚本名称,而第二个是要搜寻的词汇。然后,这个函数会以指定的文件名打开脚本文件。一旦文件打开了,这个函数就开始检查脚本文件。一次检查一行文字,每一行都会被当做字符串而读进来。

注意,每一行都会读进变量 rinStr 中,再立即复制到 inStr 中,但会去掉空白。

我们利用字符串变量 searchFor 把要搜寻的词传给 fi_GetData( ) 函数。此时在,在这个函数里,我们用 C 函数 strncmp( ) 搜寻 inStr,以期能找到所要搜寻的词。

如果要搜寻的词没有找到,这个函数就会继续读取脚本文件里的下一行文字。如果找到了,就会进入新的循环,把 inStr 变量中含有该属性值的部分,复制到名为 value 的新字符串中。接着再调用外部函数 StringToNumber( ),把这个字符串转换成整数值,然后,fi_GetData( ) 函数就返回 ivalue 的值。

描述对手行为

直接影响对手的行为是描述机制在游戏软件 AI 中最常用的方式之一。

描述行为可以让我们直接操纵 AI 对手的行为。我们需要采取某种方式让脚本能看懂游戏世界和检查条件,以改变 AI 行为。为了做到这一点,我们可以新增预先定义好的全局变量到我们的描述系统里。实际的游戏引擎会替这些变量赋值,而不是由描述语言来指定。

例如,在我们的描述系统里,我们可能有一个全局布尔变量,名叫PlayerArmed,会让胆小的巨人只敢去攻击没有武装的对手,如例8-6所示。

//例8-6:基本行为脚本

if(PlayerArmed==TRUE)
    BEGIN
        DOFlee();
    END
ELSE
    BEGIN
        DOAttack();
    END

在例8-6 中,脚本没有替 PlayerArmed 赋值,此变量是代表游戏引擎里的某个值。游戏引擎将评估此脚本,并把此行为连接到胆小的巨人身上。

描述行为的另一个方面是 AI 角色的移动。我们可以在描述系统中应用在第三章中提到的移动模式。之前在第三章中,我们直接将移动模式写进程序代码中,如果每次做出较小的修改之后都得重新编译。图8-1 是游戏设计师用描述系统实现的移动模式范例。

这里写图片描述

例8-8 显示了我们如何编写一个脚本,借此实现图8-1 中的行为。

//例8-8:移动模式脚本

if(creature.state==kPatrol)
    begin 
        move(0,1);
        move(0,1);
        move(0,1);
        move(0,1);
        move(0,1);
        move(-1,0);
        move(-1,0);
        move(0,-1);
        move(0,-1);
        move(0,-1);
        move(0,-1);
        move(0,-1);
        move(0,1);
        move(0,1);
    end

在此描述范例中,如果 AI 生物处在巡逻状态中,则使用指定的移动模式。每一步都是从前一位置移动一个单位。要了解移动模式的细节说明,参见 游戏开发中的人工智能(三):移动模式

描述口语互动

智能行为可以让游戏更富有挑战性,而口语互动也属于智能行为。游戏软件 AI 必须检查一组已知的游戏参数,并据此来做出相应。

例如,玩家的武装程度怎样,就是可以被检查的参数。然后,我们可以让敌方 AI 角色评论此种武器,看起来就好像计算机控制的角色,可以知道游戏里正在发生什么事情一样。例8-9 是说明这种脚本的简单范例。

//例8-9:口语嘲讽脚本

if(PlayerArmed == Dagger)
    Say("好可爱的小刀");
if(PlayerArmed == Bow)
    Say("放下弓就让你活命");
if(PlayerArmed == Sword)
    Say("那把剑正好可以让我当做收藏品");
if(PlayerArmed == BattleAxe)
    Say("你没有力气挥动那把战斧");

这里写图片描述

图8-2 是一个假象的游戏场景,一个巨人正在追逐玩家。就此而言,游戏软件 AI 可以使用游戏状态下特有的元素,根据当前情况,提供适当的嘲讽语。

例8-10 显示了游戏软件 AI,如何在计算机控制的巨人和玩家控制的人类之间的战斗中,找出适当的嘲讽语。

//例8-10:巨人嘲讽语脚本

if(Creature==Giant) and (player==Human)
    begin
        if(PlayerArmed == Staff)
            Say("你需要更多的棍子吧,小矮人!");
        if(PlayerArmed == Sword)
            Say("放下你的剑,否则我就打扁你!");
        if(PlayerArmed == Dagger)
            Say("你的小刀抵不上我的棍子!");

当然,这种描述机制不限于哪些要杀玩家的地方角色。友善的计算机控制角色也能利用相同的技巧。例8-11 说明了脚本如何协助剧情,并引导玩家的行为。

//例8-11:友善NPC的AI脚本

if(Creature == FreiendlyWizard)
    begin
        if(playerHas==RedAmulet)
            Say("你找到了红色护身符,把它拿到石庙,你将得到奖赏");
    end

如例8-11 所示,直到护身符被玩家得到,并且玩家面对友善的法师时,有关护身符应该放在何处的重要信息才会展现出来。

前几个脚本范例让你知道了游戏软件 AI 可以在指定情况下做出反应,但是有时候,游戏角色还需要和玩家做某种类型的口语互动。

在这种场景中,玩家必须以某种机制把文字输入给游戏。然后,游戏引擎再把文字字符串送交给描述系统,描述系统再分析文字,并提供适当的响应。图 8-3 显示了在实际游戏里的画面。

这里写图片描述

就图8-3 所示的情况而言,玩家输入了“What is your name?”,而描述系统做出响应的文字是“I am Merlin”。例8-12 是用于实现此种做法的基本脚本。

//例8-12:基本的询名脚本

If Ask("What is your name?");
begin
    Say("I am Merlin");
end

例8-12 有一个严重的缺点。只有当玩家输入和脚本中的问题完全一样的文字时,才能起作用。

检查每个口语文字字符串的另一种做法是建立语言分析器,解析每个句子,确定其到底在问什么。不过对于多数游戏来说,可以仅仅搜寻特定的关键字,并据此作出响应即可。

如例8-14 所示,脚本将检查玩家所输入的文字中,是否有“what”和“name”两个关键字存在。

//例8-14:关键字描述机制

If(Ask("what") and Ask("name"))
    begin
        Say("I am Merlin");
    end

下面将介绍如何根据指定关键字检查玩家输入的问题。例8-15就是这种做法。

//例8-15:搜寻关键字

Boolean FoundKeyword(char inputText[kStringLength],char searchFor[kStringLength])
{
    char inStr[kStringLength];
    char searchStr[kStringLength];
    int i;

    for(i=0;i<=strlen(inputText);i++)
    {
        inStr[i]=inputText[i];
        if( ((int)inStr[i]>=65) && ((int)inStr[i]<=90) )
            inStr[i]=(char)( (int)inStr[i]+32 );
    } 
    for(i=0;i<=strlen(searchFor);i++)
    {
        SearchStr[i]=searchFor[i];
        if( ((int)seachStr[i]>=65) && ((int)searchStr[i]<=90) )
            searchStr[i]=(char)( (int)searchStr[i]+32 );
    }
    if(strstr(inStr,searchStr)!=NULL)
        return true;
    return false;
}

例8-15 是游戏引擎中实际的程序代码,当游戏设计师的脚本中使用了 Ask( ) 函数时,就会被调用。这个函数有两个参数:inputText(玩家输入的文字)以及 searchFor(要搜寻的关键字)。我们在此函数中做的第一件事,就是把字符串都转换成小写。一旦有两个小写字符串,我们可以调用 C 函数 strstr()来比较两个字符串。strstr()函数会搜寻 inStr 中首次出现 searchStr 的地方,如果在 inStr 中找不到 searchStr,就会返回 false。

描述事件

本节中,我们要检视脚本如何触发与 AI 角色可能不太有直接关联的游戏事件。例如,也许站在特定位置上时,就会触发一个陷阱,如例8-16 所示。

//例8-16:陷阱事件脚本

If(PlayerLocation(120,76))
    Trigger(kExposionTrap);
If(PlayerLocation(56,16))
    Trigger(kPoisonTrap);

如例8-16 所示,描述系统可以把玩家位置和某些默认值比较,如果两者相等,就触发陷阱。

描述机制也是增加游戏气氛的有效方式,可以把某种情况或物体与特定的声音效果相连接。例8-17 说明了玩家的位置或游戏的情况(如游戏事件),触发相关音效。

//例8-17:触发音效脚本

If(PlayerLocation(kDoorway)) //玩家站在门口,播放门嘎吱响的声音
    PlaySound(kCreakingDoorSnd);
If(PlayerLocation(kDock)) //玩家在甲板走动,启用海鸥音效
    PlaySound(kSeagullSnd);
If(GameTime==kNight)      //夜晚,播放蟋蟀声
    PlaySound(kCricketsSnd);
If(GameTime==kDay)    //白天,播放鸟儿声
    PlaySound(kBirdsSnd);

猜你喜欢

转载自blog.csdn.net/Jurbo/article/details/75576847