Dota2 AI 简易开发教程(二)——英雄出装及其相关功能

距离上一篇教程也过去一个多月了,是时候写一篇新教程了。上一篇文章主要介绍了如何选择阵容和技能的使用,在这一篇文章中,我们将介绍如何配置英雄出装和其相关的一些模块。

勘误

首先先纠正一下上一篇文章中的一些错误,在选择阵容部分,下面的table.remove函数中出现了一个错误,bad argument #2 to ‘remove’ (number expected, got string),就是第二个参数应该为数字而不是字符串。这个错误将导致已选择的英雄不能从英雄列表中移除,从而会选择出重复的英雄。

function Think()for i,id in pairs(GetTeamPlayers(GetTeam())) doif(IsPlayerBot(id) and (GetSelectedHeroName(id)=="" or GetSelectedHeroName(id)==nil))thenlocal num=hero_pool_my[RandomInt(1, #hero_pool_my)]     --取随机数SelectHero( id, num );      --在保存英雄名称的表中,随机选择出AI的英雄table.remove(hero_pool_my,num)      --移除这个英雄endend
end

所以中间几行应该修改为,这样从表中才能正确移除已经选择的英雄

            local i=RandomInt(1, #hero_pool_my)     --取随机数local heroname=hero_pool_my[i]      --获取英雄的名称SelectHero( id, heroname );     --在保存英雄名称的表中,随机选择出AI的英雄table.remove(hero_pool_my,i)        --移除这个英雄

出装系统简介

在官方开发者维基中有着这样的说明:

物品购买
如果你只想重写在购买物品时的决策,你可以在文件名为item_purchase_generic.lua的文件中实现如下函数:
ItemPurchaseThink() – 每帧被调用。负责物品购买。
你也可以仅重写某个特定英雄的物品购买逻辑,比如火女(Lina),写在文件名为item_purchase_lina.lua的文件中。

dota 2 beta\game\dota\scripts\vscripts\bots_example V社的示例文件中,我们可以找到item_purchase_lina.lua文件,这就是为火女配置出装的文件。
打开这个文件,我们可以看到:

--这个表用来记录AI出装的顺序
local tableItemsToBuy = { "item_tango","item_tango","item_clarity","item_clarity","item_branches","item_branches","item_magic_stick","item_circlet","item_boots","item_energy_booster","item_staff_of_wizardry","item_ring_of_regen","item_recipe_force_staff","item_point_booster","item_staff_of_wizardry","item_ogre_axe","item_blade_of_alacrity","item_mystic_staff","item_ultimate_orb","item_void_stone","item_staff_of_wizardry","item_wind_lace","item_void_stone","item_recipe_cyclone","item_cyclone",};--------------------------------------------------------------------------------------------用来实现基本的物品购买函数
function ItemPurchaseThink()local npcBot = GetBot();if ( #tableItemsToBuy == 0 )thennpcBot:SetNextItemPurchaseValue( 0 );return;endlocal sNextItem = tableItemsToBuy[1];npcBot:SetNextItemPurchaseValue( GetItemCost( sNextItem ) );    --用于默认AI的相关函数if ( npcBot:GetGold() >= GetItemCost( sNextItem ) )         --判断金钱是否足够thennpcBot:Action_PurchaseItem( sNextItem );                --购买物品table.remove( tableItemsToBuy, 1 );endend

大家注意到,在上面那个记录出装顺序的表中,物品的名称似乎和平时用的有所区别,其实也可以在这个页面找到。

在上面这个函数中,V社为我们提供了购买物品的基本函数,但是这只能实现在泉水的商店中购买物品,如果要去神秘商店和边路商店购买物品怎么办呢?如果要配置多种出装风格应该怎么办?如何控制信使去购买物品?那就需要靠我们自己实现了。而且如果要配置物品出装,那么需要手动填写每一个配方。不过在V社最近的api更新之后,添加了一个函数GetItemComponents(sItemName),可以直接获取物品的配方。

下面,我们开始在V社的基本函数上逐渐添加更多的功能。

在神秘商店和边路商店购买装备

由于物品购买是一个通用的功能,所以我们可以在item_purchase_generic.lua中编写函数,以便重复调用。在默认AI的框架下,有两个模式用于物品购买secret_shop和side_shop,我们需要通过在主函数中控制前往神秘商店和边路商店购买物品。(尽管将选择前往哪个商店的功能耦合在一个函数中不好。更好的方法是在其它两个模式中重新编写GetDesire()函数。)

在游戏中,一个能在神秘商店购买的物品必定不能在基地商店购买,而边路商店的物品则有可能来自于其他商店。所以便能通过IsItemPurchasedFromSideShop(sNextItem)和IsItemPurchasedFromSecretShop(sNextItem )函数来判断一个物品能在哪被购买,进而决定下一个物品的购买位置。

        if(npcBot.secretShopMode~=true and npcBot.sideShopMode ~=true)thenif (IsItemPurchasedFromSideShop( sNextItem ) and npcBot:DistanceFromSideShop() <= 2000) --只有在离边路商店较近时才前往边路商店thennpcBot.sideShopMode = true;endif (IsItemPurchasedFromSecretShop( sNextItem )) thennpcBot.secretShopMode = true;endEnd

然后我们在mode_secret_shop_generic.lua和mode_side_shop_generic.lua函数中分别实现控制英雄前往神秘商店和边路商店购买的操作。
在开发者维基中提到了以下内容:

模式重写
如果你想在已有模式体系下修改某个模式逻辑的需求和行为,例如修改对线模式(laning mode),你可以在文件名为mode_laning_generic.lua的文件中实现如下函数:
GetDesire() – 每帧都被调用,需要返回一个0到1之间的浮点值,该值标志了该模式有多大可能成为当前激活模式
OnStart() – 当该模式成为当前激活模式时调用
OnEnd() – 当该模式让出控制权给其他被激活模式时调用
Think() – 当该模式为当前激活模式时,每帧都被调用。负责发出机器人的行为指令。
你也可以仅重写某个特定英雄的模式逻辑,比如火女(Lina),写在文件名为mode_laning_lina.lua的文件中。如果你想在特定英雄的模式重写时调用泛用的英雄模式代码,请参考附件A的实现细节。

所以,我们便需要GetDesire()和Think()以重写该模式。当英雄离边路商店较近,而且下一件物品可以在边路商店购买时,英雄便不需要用信使或自己返回基地购买物品,而是直接前往边路商店。
mode_side_shop_generic.lua 边路商店模式

function GetDesire()local npcBot = GetBot();local desire = 0.0;if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() )     --不应打断持续施法thenreturn 0endif ( npcBot.sideShopMode == true and npcBot:GetGold() >= npcBot:GetNextItemPurchaseValue()) thenlocal d=npcBot:DistanceFromSideShop()if d<2000thendesire = (2000-d)/d*0.3+0.3;                    --根据离边路商店的距离返回欲望值endelsenpcBot.sideShopMode = falseendreturn desireendfunction Think()local npcBot = GetBot();local shopLoc1 = GetShopLocation( GetTeam(), SHOP_SIDE );local shopLoc2 = GetShopLocation( GetTeam(), SHOP_SIDE2 );if ( GetUnitToLocationDistance(npcBot, shopLoc1) <= GetUnitToLocationDistance(npcBot, shopLoc2) ) then  --选择前往距离自己更近的商店npcBot:Action_MoveToLocation( shopLoc1 );elsenpcBot:Action_MoveToLocation( shopLoc2 );end
end

mode_secret_shop_generic.lua 神秘商店模式

function GetDesire()local npcBot = GetBot();local desire = 0.0;if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() )     --不应打断持续施法thenreturn 0endif ( npcBot.secretShopMode == true and npcBot:GetGold() >= npcBot:GetNextItemPurchaseValue()) thenlocal d=npcBot:DistanceFromSecretShop()if d<3000thendesire = (3000-d)/d*0.3+0.3;                --根据离边路商店的距离返回欲望值endelsenpcBot.secretShopMode = falseendreturn desireendfunction Think()local npcBot = GetBot();local shopLoc1 = GetShopLocation( GetTeam(), SHOP_SECRET );local shopLoc2 = GetShopLocation( GetTeam(), SHOP_SECRET2 );if ( GetUnitToLocationDistance(npcBot, shopLoc1) <= GetUnitToLocationDistance(npcBot, shopLoc2) ) then  --选择前往距离自己更近的商店npcBot:Action_MoveToLocation( shopLoc1 );elsenpcBot:Action_MoveToLocation( shopLoc2 );endend

然后需要在主购买函数中检查英雄是否已经到达神秘或野外商店附近,并购买物品。

        local PurchaseResult        --接收购买结果,后文会介绍if(npcBot.sideShopMode == true)thenif(npcBot:DistanceFromSideShop() <= 200)thenPurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )endelseif(npcBot.secretShopMode == true)       --如果目标是神秘商店,则命令信使购买物品thenif(npcBot:DistanceFromSecretShop() <= 200)thenPurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )endlocal courier=GetCourier(0)if(courier==nil)thenBuyCourier()        --没有信使的话则会购买elseif(courier:DistanceFromSecretShop() <= 200)     --信使已到达商店thenPurchaseResult=GetCourier(0):ActionImmediate_PurchaseItem( sNextItem )endendelsePurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )end

购买信使的函数

function BuyCourier()local npcBot=GetBot()local courier=GetCourier(0)if(courier==nil)        --购买小鸡thenif(npcBot:GetGold()>=GetItemCost("item_courier"))thenlocal info=npcBot:ActionImmediate_PurchaseItem("item_courier");if info ==PURCHASE_ITEM_SUCCESS thenprint(npcBot:GetUnitName()..' buy the courier',info);endendelse                    --购买飞行信使if DotaTime()>60*3 and npcBot:GetGold()>=GetItemCost("item_flying_courier") and (courier:GetMaxHealth()==75) thenlocal info=npcBot:ActionImmediate_PurchaseItem("item_flying_courier");if info ==PURCHASE_ITEM_SUCCESS thenprint(npcBot:GetUnitName()..' has upgraded the courier.',info);endendend
end

检查购买结果

npcBot:Action_PurchaseItem( sNextItem )函数会返回一个购买结果的值,其可能的值为:

    Item Purchase ResultsPURCHASE_ITEM_SUCCESS               --成功购买PURCHASE_ITEM_OUT_OF_STOCK          --物品栏已满PURCHASE_ITEM_DISALLOWED_ITEM       --不被允许的物品 PURCHASE_ITEM_INSUFFICIENT_GOLD     --金钱不足 PURCHASE_ITEM_NOT_AT_HOME_SHOP      --不在基地商店 PURCHASE_ITEM_NOT_AT_SIDE_SHOP      --不在边路商店PURCHASE_ITEM_NOT_AT_SECRET_SHOP    --不在神秘商店PURCHASE_ITEM_INVALID_ITEM_NAME     --不存在的物品名称

在购买物品时,用一个变量记录购买结果,并随后检查,以免出现购买失败的情况。
local PurchaseResult=npcBot:ActionImmediate_PurchaseItem( sNextItem )

        if(PurchaseResult==PURCHASE_ITEM_SUCCESS)       --成功购买便从出装表中移除该物品thennpcBot.secretShopMode = false;npcBot.sideShopMode = false;table.remove( ItemsToBuy, 1 )endif(PurchaseResult==PURCHASE_ITEM_OUT_OF_STOCK)  --物品栏已满,出售多余的物品thenSellExtraItem()endif(PurchaseResult==PURCHASE_ITEM_INVALID_ITEM_NAME or PurchaseResult==PURCHASE_ITEM_DISALLOWED_ITEM)    --不存在的物品,移除该物品thentable.remove( ItemsToBuy, 1 )endif(PurchaseResult==PURCHASE_ITEM_INSUFFICIENT_GOLD )    --金额不足(其实该情况也较少出现,因为我们已经在上面判断了金钱)thennpcBot.secretShopMode = false;npcBot.sideShopMode = false;endif(PurchaseResult==PURCHASE_ITEM_NOT_AT_SECRET_SHOP)    --不在神秘商店,前往神秘商店thennpcBot.secretShopMode = truenpcBot.sideShopMode = false;endif(PurchaseResult==PURCHASE_ITEM_NOT_AT_SIDE_SHOP)      --不在边路商店(其实该情况不会出现,因为在边路商店的物品能在其他商店购买)thennpcBot.sideShopMode = truenpcBot.secretShopMode = false;endif(PurchaseResult==PURCHASE_ITEM_NOT_AT_HOME_SHOP)      --不在基地商店(也不会出现的情况,因为如果英雄不在家中,那么物品会购买于贮藏处)thennpcBot.secretShopMode = false;npcBot.sideShopMode = false;end

两个效用函数,用于判断物品栏是否已满和出售特定物品

function SellSpecifiedItem( item_name )     --出售特定物品local npcBot = GetBot();local itemCount = 0;local item = nil;for i = 0, 14 dolocal sCurItem = npcBot:GetItemInSlot(i);if ( sCurItem ~= nil ) thenitemCount = itemCount + 1;if ( sCurItem:GetName() == item_name ) thenitem = sCurItem;endendendif ( item ~= nil and itemCount > 5 and (npcBot:DistanceFromFountain() <= 600 or npcBot:DistanceFromSideShop() <= 200 or npcBot:DistanceFromSecretShop() <= 200) ) thennpcBot:ActionImmediate_SellItem( item );endendfunction SellExtraItem()        --出售已经没有用处的物品if(GameTime()>15*60)thenSellSpecifiedItem("item_faerie_fire")SellSpecifiedItem("item_enchanted_mango")SellSpecifiedItem("item_tango")SellSpecifiedItem("item_clarity")SellSpecifiedItem("item_flask")endif(GameTime()>20*60)thenSellSpecifiedItem("item_stout_shield")SellSpecifiedItem("item_orb_of_venom")endif(GameTime()>30*60)thenSellSpecifiedItem("item_branches")SellSpecifiedItem("item_bottle")SellSpecifiedItem("item_magic_wand")SellSpecifiedItem("item_magic_stick")SellSpecifiedItem("item_urn_of_shadows")SellSpecifiedItem("item_drums_of_endurance")end
end

信使控制

信使又称为小鸡,一直都是dota中有很大用处的单位,一直以来都是各方神圣追杀的对象。但是因为7.00版本刚出时,默认AI的信使控制机制有些问题,不知道现在修复了没有。而且为了与我们之前所写的神秘商店控制系统配合,所以需要重写信使控制模块。
在前文中我们提到过,信使控制的函数位于ability_item_usage_generic.lua的CourierUsageThink()中,所以我们只要在这个文件中重写此函数,便能完全地控制信使。

信使有以下几种状态和命令,参见开发者维基。通过ActionImmediate_Courier( hCourier, nAction ) 函数命令信使,int GetCourierState( hCourier )函数则能获取信使的状态。

Courier Actions and StatesCOURIER_ACTION_BURST                --加速COURIER_ACTION_ENEMY_SECRET_SHOP    --前往敌方的神秘商店COURIER_ACTION_RETURN               --返回基地COURIER_ACTION_SECRET_SHOP          --前往神秘商店COURIER_ACTION_SIDE_SHOP            --前往边路商店COURIER_ACTION_SIDE_SHOP2           --前往边路商店COURIER_ACTION_TAKE_STASH_ITEMS     --拾起物品COURIER_ACTION_TAKE_AND_TRANSFER_ITEMS  --拾起并运送物品COURIER_ACTION_TRANSFER_ITEMS       --运送物品COURIER_STATE_IDLE                  --空闲COURIER_STATE_AT_BASE               --处于基地COURIER_STATE_MOVING                --移动COURIER_STATE_DELIVERING_ITEMS      --运送物品COURIER_STATE_RETURNING_TO_BASE     --返回基地COURIER_STATE_DEAD                  --信使已死亡

所以信使控制模块需要先检测信使的状态,然后再根据英雄是否有装备需要运送来决定信使的行为。
以下便是信使控制主函数

function CourierUsageThink()local npcBot=GetBot()local courier=GetCourier(0)     --获取信使句柄if(npcBot:IsAlive()==false or courier==nil or npcBot:IsHero()==false or npcBot:HasModifier("modifier_arc_warden_tempest_double"))   --判断使用者是不是真正的英雄thenreturnendlocal state=GetCourierState(courier)        --获取信使状态local burst=courier:GetAbilityByName("courier_burst")local CanCastBurst=burst~=nil and burst:IsFullyCastable()      --检查信使加速能否使用if(state == COURIER_STATE_DEAD)     --信使已死亡thenreturnendif(courier:GetHealth()/courier:GetMaxHealth()<=0.9)     --信使受到攻击,立刻回家thenif(CanCastBurst)thennpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_BURST)endnpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_RETURN)returnendif(state == COURIER_STATE_IDLE)     --信使处于空闲状态10秒以上则回家thenif(courier.idletime==nil)thencourier.idletime=GameTime()elseif(GameTime()-courier.idletime>10)thennpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_RETURN)courier.idletime=nilreturnendendendif(state == COURIER_STATE_RETURNING_TO_BASE and npcBot:GetCourierValue()>0 and not utility.IsItemSlotsFull())       --运送物品thennpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_TRANSFER_ITEMS)returnendif (state == COURIER_STATE_AT_BASE )        --从基地运送thenif( npcBot:GetStashValue() >= 400)thenif(courier.time==nil)thencourier.time=DotaTime()endif(courier.time+1<DotaTime())thennpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_TAKE_AND_TRANSFER_ITEMS)courier.time=nilendreturnendendif(state == COURIER_STATE_AT_BASE or state == COURIER_STATE_RETURNING_TO_BASE)      --前往神秘商店thenif(npcBot.secretShopMode == true and npcBot:GetActiveMode() ~= BOT_MODE_SECRET_SHOP)thennpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_SECRET_SHOP)returnendendif(state == COURIER_STATE_DELIVERING_ITEMS)     --当信使正在运送物品时加速thenif(CanCastBurst)thennpcBot:ActionImmediate_Courier(courier, COURIER_ACTION_BURST)endendend

最后需要每个英雄调用主函数。例如ability_item_usage_zuus.lua。

function CourierUsageThink()ability_item_usage_generic.CourierUsageThink()
end

示例

为宙斯配置的出装:ability_item_usage_zuus.lua

require( GetScriptDirectory().."/item_purchase_generic" ) local ItemsToBuy = 
{ "item_tango","item_clarity","item_branches","item_branches","item_faerie_fire","item_bottle","item_boots","item_energy_booster",          --秘法鞋"item_mantle","item_circlet","item_recipe_null_talisman",    --无用挂件"item_mantle","item_circlet","item_recipe_null_talisman",    --无用挂件"item_helm_of_iron_will","item_recipe_veil_of_discord",  --纷争"item_void_stone","item_energy_booster","item_recipe_aether_lens",      --以太之镜7.06"item_staff_of_wizardry","item_void_stone","item_recipe_cyclone","item_wind_lace",               --风杖"item_point_booster","item_staff_of_wizardry","item_ogre_axe","item_blade_of_alacrity",       --蓝杖"item_point_booster","item_vitality_booster","item_energy_booster","item_mystic_staff",            --玲珑心
}function ItemPurchaseThink()purchase.ItemPurchase(ItemsToBuy)       --将出装表传至主函数
end

总结

看完了这篇文章后,大家应该能够了解AI出装的相关功能了,虽然说这篇文章因为作者太懒拖了很久,不过还是要比V社好一点。V社说好的战役第二章迟迟没有推出,即将重新定义“七月下旬”。
下面上张图。
出装效果图

附件

1.示例代码

Published by

风君子

独自遨游何稽首 揭天掀地慰生平