第一篇教程中,我们主要介绍了Dota2 AI的基本情况,在这篇文章中,我们将介绍如何为AI选择阵容和技能使用(以宙斯为例)。

选择阵容

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

如果你想控制英雄选择和分路,你可以在文件名为hero_selection.lua的文件中实现如下函数:
Think() - 每帧被调用。负责机器人选择英雄。
UpdateLaneAssignments() - 在游戏开始前的每一帧被调用。返回玩家ID-分路的值对。
GetBotNames() - 调用一次,返回一个包含玩家名称的表。

dota 2 beta\game\dota\scripts\vscripts\bots_exampleV社的示例文件中,我们可以找到hero_selection.lua文件,这就是为AI选择阵容的脚本文件。

如果我们要开始制作自己的AI,那么首先需要创建dota 2 beta\game\dota\scripts\vscripts\bots文件夹,之后建立hero_selection.lua文件,在其中编写选择阵容的部分,这样的话AI就会选择自定义阵容了。

打开这个文件,我们可以看到如下内容

    function Think()
        if ( GetTeam() == TEAM_RADIANT )                    --处于天辉方时
        then
            print( "selecting radiant" );                    --在控制台输出调试信息
            SelectHero( 0, "npc_dota_hero_antimage" );        --为0号玩家选择敌法师
            SelectHero( 1, "npc_dota_hero_axe" );
            SelectHero( 2, "npc_dota_hero_bane" );
            SelectHero( 3, "npc_dota_hero_bloodseeker" );
            SelectHero( 4, "npc_dota_hero_crystal_maiden" );
        elseif ( GetTeam() == TEAM_DIRE )
        then
            print( "selecting dire" );
            SelectHero( 5, "npc_dota_hero_drow_ranger" );
            SelectHero( 6, "npc_dota_hero_earthshaker" );
            SelectHero( 7, "npc_dota_hero_juggernaut" );
            SelectHero( 8, "npc_dota_hero_mirana" );
            SelectHero( 9, "npc_dota_hero_nevermore" );
        end
    end

其中Think()是主函数,游戏选择阵容时就会调用这个函数,然后判断是天辉方还是夜魇方,之后分别选择各自的阵容。那么我们如何找到英雄对应的名字呢?在这里便可找到英雄的实际名称。

例如宙斯的实际名称是"npc_dota_hero_zuus",那么我们将夜魇方的SelectHero( 5, "npc_dota_hero_drow_ranger" );替换为SelectHero( 5, "npc_dota_hero_zuus" );,那么AI便会选择在5号玩家的位置上选择宙斯。需要注意的是,玩家的ID是会随着人类玩家的加入而发生改变的,将玩家ID硬编码是一种不好的做法,比较好的方法是通过循环遍历出玩家的ID,这样无论人类玩家有多少,AI都能正常选择阵容。

下面便是能够自动适应玩家变化选择阵容的代码。

function Think()
        for i,id in pairs(GetTeamPlayers(GetTeam()))    --Lua语言自带的迭代器pairs,能够依次读取表中的值。
        do
            if(IsPlayerBot(id))                                     --判断该玩家是否为电脑
            then
                SelectHero( id, "npc_dota_hero_drow_ranger" );      --为id号玩家选择黑暗游侠
            end
        end
    end

如果我们要AI能够选择随机的阵容,而不是仅仅选择10个黑暗游侠,那该怎么办呢?很简单,通过Lua语言唯一的数据结构表(称作Table,可以用作数组)保存可以选择的英雄列表,然后随机出一个序号,选出表中的英雄。

hero_pool_my=
{
        'npc_dota_hero_zuus',
        'npc_dota_hero_skywrath_mage',
        'npc_dota_hero_ogre_magi',
        'npc_dota_hero_chaos_knight',
        'npc_dota_hero_viper',
        "npc_dota_hero_lina",
        "npc_dota_hero_kunkka",
        "npc_dota_hero_lich",
        "npc_dota_hero_shadow_shaman",
        "npc_dota_hero_centaur",
        'npc_dota_hero_crystal_maiden',
}
function Think()
    for i,id in pairs(GetTeamPlayers(GetTeam())) 
    do
        if(IsPlayerBot(id) and (GetSelectedHeroName(id)=="" or GetSelectedHeroName(id)==nil))
        then
            local num=hero_pool_my[RandomInt(1, #hero_pool_my)]     --取随机数
            SelectHero( id, num );          --在保存英雄名称的表中,随机选择出AI的英雄
            table.remove(hero_pool_my,num)      --移除这个英雄
        end
    end
end

以上代码会为两边的AI依次从hero_pool_my表中随机选出5个英雄。如果要让AI不选择重复的英雄,那么还需要进一步的修改。

选择分路

为AI分路的函数也是在hero_selection.lua中,如果我们想让AI遵循自定义的分路,那么需要在这个文件里新建一个UpdateLaneAssignments()函数。这个函数是用于游戏底层C++端调用,所以需要返回一个分路情况的表。

function UpdateLaneAssignments()
    local npcBot = GetBot()
    local lanes=
    {
        [1]=LANE_MID,
        [2]=LANE_BOT,
        [3]=LANE_TOP,
        [4]=LANE_BOT,
        [5]=LANE_TOP,
    }
    return lanes;
end

以上函数根据AI的位置分路,1楼中路,2楼下路,3楼上路,4楼下路,5楼上路。当然,如果要根据英雄的特性分路,那么需要加入更多判断。如果没有自己写好分路系统的话,可以暂时启用默认分路系统,即不编写这个函数。

技能加点

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

如果你只想重写在技能和物品使用时的决策,你可以在文件名为ability_item_usage_generic.lua的文件中实现如下函数:
ItemUsageThink() - 每帧被调用。负责发出物品使用行为。
AbilityUsageThink() - 每帧被调用。负责发出技能使用行为。
CourierUsageThink() - 每帧被调用。负责发出信使的相关命令。
BuybackUsageThink() - 每帧被调用。负责发出买活指令。
AbilityLevelUpThink() - 每帧被调用。负责升级技英雄能。
这些函数中未被重写的,会自动采用默认的C++实现。
你也可以仅重写某个特定英雄对技能/物品使用的逻辑,比如火女(Lina),写在文件名为ability_item_usage_lina.lua的文件中。如果你想在特定英雄对技能/物品使用的逻辑重写时调用泛用的逻辑代码,请参考附件A的实现细节。

我们要实现的第一个英雄为宙斯,那么就可以新建一个ability_item_usage_zuus.lua的文件。首先要实现AI的加点,就要在此文件中编写AbilityLevelUpThink()函数。然后通过 npcBot:ActionImmediate_LevelAbility(abilityname);升级名称为abilityname的技能。那么怎样获取升级技能所需的技能名称呢?比较原始的方法是用一个表记录从1到25级所有的技能名称。其中的数据要在dota 2 beta\game\dota\scripts\npc\npc_heroes.txt中获取。
例如宙斯的技能为

    --以下为宙斯的技能名称
    "zuus_arc_lightning",
    "zuus_lightning_bolt",
    "zuus_static_field",
    "zuus_cloud",
    "zuus_thundergods_wrath",
    --以下为某个版本的天赋,请注意天赋名称会随着版本更新而变化,所以以下部分可能在当前版本中无效
    "special_bonus_mp_regen_2",
    "special_bonus_movement_speed_25",
    "special_bonus_armor_7",
    "special_bonus_magic_resistance_15",
    "special_bonus_unique_zeus_2",
    "special_bonus_unique_zeus_3",
    "special_bonus_cast_range_200",
    "special_bonus_unique_zeus",

如果通过原始方法记录,则需要排列组合以上的技能天赋。比较好的方法则是用两个表分别保存技能和天赋的名称,然后在一个统一的表中引用技能天赋名称,以存储加点顺序。

local Talents =
{
    "special_bonus_mp_regen_2",
    "special_bonus_movement_speed_25",
    "special_bonus_armor_7",
    "special_bonus_magic_resistance_15",
    "special_bonus_unique_zeus_2",
    "special_bonus_unique_zeus_3",
    "special_bonus_cast_range_200",
    "special_bonus_unique_zeus",
}

local Abilities =
{
    "zuus_arc_lightning",
    "zuus_lightning_bolt",
    "zuus_static_field",
    "zuus_cloud",
    "zuus_thundergods_wrath",
}
--存储技能句柄,方便调用
local npcBot = GetBot()
local AbilitiesReal =
{
    npcBot:GetAbilityByName(Abilities[1]),
    npcBot:GetAbilityByName(Abilities[2]),
    npcBot:GetAbilityByName(Abilities[3]),
    npcBot:GetAbilityByName(Abilities[4]),
    npcBot:GetAbilityByName(Abilities[5])
}
--存储加点顺序
local AbilityToLevelUp=
{
    Abilities[1],
    Abilities[3],
    Abilities[2],
    Abilities[2],
    Abilities[2],
    Abilities[5],
    Abilities[2],
    Abilities[1],
    Abilities[1],
    Talents[1],
    Abilities[1],
    Abilities[5],
    Abilities[3],
    Abilities[3],
    Talents[3],
    Abilities[3],
    Abilities[5],
    Talents[5],
    Talents[7],
}

至此,英雄的技能天赋名称和加点顺序都已完成存储,接下来只需要在函数中引用加点顺序即可完成加点。

function AbilityLevelUpThink()
    local npcBot = GetBot();
    
    if npcBot:GetAbilityPoints()<1 or #AbilityToLevelUp==0--当没有技能点时,或没有技能可升时不继续执行。
    then
        return;
    end
    
    local ability=npcBot:GetAbilityByName(AbilityToLevelUp[1]); --获取技能句柄
    
    if ability~=nil and ability:CanAbilityBeUpgraded()          --判断技能可否升级
    then
        npcBot:ActionImmediate_LevelAbility(AbilityToLevelUp[1]);   --升级技能
        table.remove( AbilityToLevelUp, 1 );                --从表中移除刚刚升级的技能
    end
end

我们之前说过,天赋名称会随着版本更新而不断变化,而且每次手动从npc_heroes.txt中获取技能天赋名称也有点麻烦,那么有什么更简便的方法呢?
当然有,比如通过遍历英雄的技能槽来获取每一个技能和天赋的名称。

local Talents ={}
local Abilities ={}
local npcBot = GetBot()

for i=0,23,1 do                                 --由于卡尔有24个技能,所以需要从0遍历至23
    local ability=npcBot:GetAbilityInSlot(i)
    if(ability~=nil)
    then
        if(ability:IsTalent()==true)
        then
            table.insert(Talents,ability:GetName())
        else
            table.insert(Abilities,ability:GetName())
        end
    end
end

这样的话,我们每次新编写一个英雄,就不用手动去复制技能名称了。

使用技能

示例代码

如果要重新编写一个英雄的技能使用,则需要编写AbilityUsageThink()函数。
打开示例文件ability_item_usage_lina.lua,我们可以看到如下代码

castLBDesire = 0;
castLSADesire = 0;
castDSDesire = 0;

function AbilityUsageThink()

    local npcBot = GetBot();

    -- Check if we're already using an ability 检查我们是否正在使用技能
    if ( npcBot:IsUsingAbility() ) then return end;

    abilityLSA = npcBot:GetAbilityByName( "lina_light_strike_array" );
    abilityDS = npcBot:GetAbilityByName( "lina_dragon_slave" );
    abilityLB = npcBot:GetAbilityByName( "lina_laguna_blade" );

    -- Consider using each ability  考虑使用每一个技能,得到使用技能的欲望值和技能目标
    castLBDesire, castLBTarget = ConsiderLagunaBlade();
    castLSADesire, castLSALocation = ConsiderLightStrikeArray();
    castDSDesire, castDSLocation = ConsiderDragonSlave();

    if ( castLBDesire > castLSADesire and castLBDesire > castDSDesire ) 
    then
        npcBot:Action_UseAbilityOnEntity( abilityLB, castLBTarget );
        return;
    end

    if ( castLSADesire > 0 ) 
    then
        npcBot:Action_UseAbilityOnLocation( abilityLSA, castLSALocation );
        return;
    end

    if ( castDSDesire > 0 ) 
    then
        npcBot:Action_UseAbilityOnLocation( abilityDS, castDSLocation );
        return;
    end

end

----------------------------------------------------------------------------------------------------
--判断技能能否对目标使用
function CanCastLightStrikeArrayOnTarget( npcTarget )
    return npcTarget:CanBeSeen() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable();
end


function CanCastDragonSlaveOnTarget( npcTarget )
    return npcTarget:CanBeSeen() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable();
end


function CanCastLagunaBladeOnTarget( npcTarget )
    return npcTarget:CanBeSeen() and npcTarget:IsHero() and ( GetBot():HasScepter() or not npcTarget:IsMagicImmune() ) and not npcTarget:IsInvulnerable();
end

----------------------------------------------------------------------------------------------------
--考虑是否使用2技能
function ConsiderLightStrikeArray()

    local npcBot = GetBot();

    -- Make sure it's castable      确认技能能否使用
    if ( not abilityLSA:IsFullyCastable() ) 
    then 
        return BOT_ACTION_DESIRE_NONE, 0;
    end;

    -- If we want to cast Laguna Blade at all, bail     如果要使用大招,那么先不用这个技能
    if ( castLBDesire > 0 ) 
    then
        return BOT_ACTION_DESIRE_NONE, 0;
    end

    -- Get some of its values       获取技能相关的参数
    local nRadius = abilityLSA:GetSpecialValueInt( "light_strike_array_aoe" );
    local nCastRange = abilityLSA:GetCastRange();
    local nDamage = abilityLSA:GetAbilityDamage();

    --------------------------------------
    -- Global high-priorty usage
    --------------------------------------

    -- Check for a channeling enemy     打断正在吟唱的敌人
    local tableNearbyEnemyHeroes = npcBot:GetNearbyHeroes( nCastRange + nRadius + 200, true, BOT_MODE_NONE );
    for _,npcEnemy in pairs( tableNearbyEnemyHeroes )
    do
        if ( npcEnemy:IsChanneling() ) 
        then
            return BOT_ACTION_DESIRE_HIGH, npcEnemy:GetLocation();
        end
    end

    --------------------------------------
    -- Mode based usage
    --------------------------------------

    -- If we're farming and can kill 3+ creeps with LSA     收兵线打钱
    if ( npcBot:GetActiveMode() == BOT_MODE_FARM ) then
        local locationAoE = npcBot:FindAoELocation( true, false, npcBot:GetLocation(), nCastRange, nRadius, 0, nDamage );

        if ( locationAoE.count >= 3 ) then
            return BOT_ACTION_DESIRE_LOW, locationAoE.targetloc;
        end
    end

    -- If we're pushing or defending a lane and can hit 4+ creeps, go for it        推进与防守
    if ( npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_TOP or
         npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_MID or
         npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_BOT or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_TOP or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_MID or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_BOT ) 
    then
        local locationAoE = npcBot:FindAoELocation( true, false, npcBot:GetLocation(), nCastRange, nRadius, 0, 0 );

        if ( locationAoE.count >= 4 ) 
        then
            return BOT_ACTION_DESIRE_LOW, locationAoE.targetloc;
        end
    end

    -- If we're seriously retreating, see if we can land a stun on someone who's damaged us recently        逃跑时阻止敌人追击
    if ( npcBot:GetActiveMode() == BOT_MODE_RETREAT and npcBot:GetActiveModeDesire() >= BOT_MODE_DESIRE_HIGH ) 
    then
        local tableNearbyEnemyHeroes = npcBot:GetNearbyHeroes( nCastRange + nRadius + 200, true, BOT_MODE_NONE );
        for _,npcEnemy in pairs( tableNearbyEnemyHeroes )
        do
            if ( npcBot:WasRecentlyDamagedByHero( npcEnemy, 2.0 ) ) 
            then
                if ( CanCastLightStrikeArrayOnTarget( npcEnemy ) ) 
                then
                    return BOT_ACTION_DESIRE_MODERATE, npcEnemy:GetLocation();
                end
            end
        end
    end

    -- If we're going after someone     对当前目标使用技能
    if ( npcBot:GetActiveMode() == BOT_MODE_ROAM or
         npcBot:GetActiveMode() == BOT_MODE_TEAM_ROAM or
         npcBot:GetActiveMode() == BOT_MODE_GANK or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_ALLY ) 
    then
        local npcTarget = npcBot:GetTarget();

        if ( npcTarget ~= nil ) 
        then
            if ( CanCastLightStrikeArrayOnTarget( npcTarget ) )
            then
                return BOT_ACTION_DESIRE_HIGH, npcTarget:GetLocation();
            end
        end
    end

    return BOT_ACTION_DESIRE_NONE, 0;       --不使用技能
end

function ConsiderDragonSlave()
    --略,考虑是否使用1技能
end


function ConsiderLagunaBlade()
    --略,考虑是否使用4技能
end


以上便是原版AI的火女技能使用源代码,当然这是由C++代码翻译过来的。我们要编写的代码结构也可以参考这一部分,主要分为3个部分:

  1. 主思考函数
  2. 判断技能能否对目标使用
  3. 考虑每一个技能
    调用关系为:主思考函数->考虑每一个技能->判断技能能否对目标使用

实战

在看完了示例代码后,我们应该对英雄的技能使用有了一个初步的了解,那么接下来便介绍一下如何从零开始开发一个新的英雄。(以宙斯为例)

主思考函数

首先先编写主思考函数,每一个英雄的主思考函数都是相似的,只需要处理每个技能不同类型的目标即可。
在函数开头用local npcBot = GetBot()记录此英雄的句柄,方便后面的调用。
之后需要判断英雄能否使用技能,是正在施法,还是被沉默了。

    if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() or npcBot:IsSilenced() )
    then 
        return
    end

然后获取一些常用的量

    AttackRange=npcBot:GetAttackRange()     --获取攻击距离
    ManaPercentage=npcBot:GetMana()/npcBot:GetMaxMana()     --获取当前魔法百分比
    HealthPercentage=npcBot:GetHealth()/npcBot:GetMaxHealth()       --获取当前生命百分比

然后考虑每一个技能的使用,Lua语言支持多重返回值,所以能够方便地从函数获取结果。

    cast1Desire, cast1Target = Consider1();     --1技能为单位目标技能
    cast2Desire, cast2Target, cast2TargetType = Consider2();        --2技能为单位目标技能或地点目标技能
    cast5Desire = Consider5();      --大招为无目标技能
    cast4Desire, cast4Location = Consider4();       --5技能(A杖技能)是地点目标技能

最后使用每个技能,从上到下,为技能使用的优先级。当然我们也可以通过比较每个技能的使用欲望,选择欲望值最高的技能使用。

    if ( cast4Desire > 0 ) 
    then
        npcBot:Action_UseAbility( AbilitiesReal[4] );       --使用大招
        return
    end
    
    if ( cast5Desire > 0 ) 
    then
        npcBot:Action_UseAbilityOnLocation( AbilitiesReal[5], cast5Location );      --对目标点使用5技能
        return
    end
    
    if ( cast2Desire > 0 ) 
    then
        if(cast2TargetType=="target")
        then
            npcBot:Action_UseAbilityOnEntity( AbilitiesReal[2], cast2Target );      --对单位使用2技能
            return
        elseif(cast2TargetType=="location")
        then
            npcBot:Action_UseAbilityOnLocation( AbilitiesReal[2], cast2Target );        --对目标点使用2技能
            return
        end
    end
    
    if ( cast1Desire > 0 ) 
    then
        npcBot:Action_UseAbilityOnEntity(AbilitiesReal[1], cast1Target );       --对目标单位使用1技能
        return
    end

至此,主思考函数编写完毕。

考虑使用每一个技能

在开始开发一个技能的使用前,我们应该先想想,这个技能有什么用?是一个单体爆发技能,还是一个范围技能?这个技能有没有控制效果?这样的话才好编写接下来的技能。
比如说宙斯的1技能,可以用来补兵,清兵,对英雄造成范围伤害等等。
首先我们也需要获取英雄和技能的句柄。在函数开头用local npcBot = GetBot()记录此英雄的句柄,用local ability=AbilitiesReal[1];记录此技能的句柄。
然后需要判断这个技能能否使用,即魔法够不够,冷却有没有好。

    if not ability:IsFullyCastable() then
        return BOT_ACTION_DESIRE_NONE, 0;
    end

之后也是记录一些常用的量,以免重复获取降低效率。

    local CastRange = ability:GetCastRange();       --获取施法距离
    local Damage = ability:GetAbilityDamage();      --获取技能伤害,注意这个函数不一定有效...有些特殊的技能无法获取
    
    local HeroHealth=10000
    local CreepHealth=10000
    local allys = npcBot:GetNearbyHeroes( 1200, false, BOT_MODE_NONE );     --获取周围友方英雄
    local enemys = npcBot:GetNearbyHeroes(CastRange+300,true,BOT_MODE_NONE)     --获取周围敌方英雄
    local WeakestEnemy,HeroHealth=utility.GetWeakestUnit(enemys)        --获取血量最低的敌方英雄
    local creeps = npcBot:GetNearbyCreeps(CastRange+300,true)       --获取周围的敌方小兵
    local WeakestCreep,CreepHealth=utility.GetWeakestUnit(creeps)       --获取血量最低的敌方小兵

注意,在这个地方我调用了utility函数库中的函数,调用函数库模块,需要在脚本开头加入require(GetScriptDirectory() .. "/utility")。当然这个utility名字是可以变化的。该函数的实际代码放在文末。

1.之后,便是宙斯1技能的实际用途,我分为了5个部分:

  • 团战使用
  • 线上正补
  • 发育打钱
  • 推进防守
  • 通用

function Consider1()

(1) 团战使用
当英雄模式处于攻击时,判断蓝量是否足够,敌方单位数量是否足够,然后找到最脆弱的敌方单位或者英雄,判断它们能不能被技能作用,最后将这个单位的句柄返回给上层主思考函数。

--teamfightUsing
    if(npcBot:GetActiveMode() == BOT_MODE_ATTACK )
    then
        if(ManaPercentage>0.4 and #enemys+#creeps>2)        --蓝量大于40%,敌方单位数>2
        then
            if(WeakestCreep~=nil and CanCast1( WeakestCreep ))      --CanCast1是一个自定义函数,用于判断技能能不能对目标使用,请参考下一部分。
            then
                return BOT_ACTION_DESIRE_HIGH,WeakestCreep;
            end
            if(WeakestEnemy~=nil and CanCast1( WeakestEnemy ))
            then
                return BOT_ACTION_DESIRE_HIGH,WeakestEnemy; 
            end
        end
    end

(2) 线上正补

    --Last hit
    if ( npcBot:GetActiveMode() == BOT_MODE_LANING )
    then
        if(WeakestCreep~=nil)
        then
            if(ManaPercentage>0.4 and GetUnitToUnitDistance(npcBot,WeakestCreep)>=AttackRange-ManaPercentage)       --蓝量大于40%,而且该单位远离英雄的攻击距离
            then
                if(CreepHealth<=WeakestCreep:GetActualIncomingDamage(Damage,DAMAGE_TYPE_MAGICAL))       --伤害是否足够
                then
                    return BOT_ACTION_DESIRE_LOW,WeakestCreep; 
                end
            end 
        end
    end

(3) 发育打钱

-- If we're farming and can hit 2+ creeps and kill 1+ 
    if ( npcBot:GetActiveMode() == BOT_MODE_FARM )
    then
        if ( #creeps >= 2 )--单位是不是足够多
        then
            if (CreepHealth<=WeakestCreep:GetActualIncomingDamage(Damage,DAMAGE_TYPE_MAGICAL))--伤害是否足够
            then
                return BOT_ACTION_DESIRE_LOW, WeakestCreep;
            end
        end
    end

(4) 推进防守

    -- If we're pushing or defending a lane and can hit 3+ creeps, go for it
    if ( npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_TOP or
         npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_MID or
         npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_BOT or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_TOP or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_MID or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_BOT ) 
    then
        if ( #enemys+#creeps > 2 and ManaPercentage>0.4)--蓝量大于40%,敌方单位数>2
        then
            if (WeakestEnemy~=nil)
            then
                if ( CanCast1( WeakestEnemy )and GetUnitToUnitDistance(npcBot,WeakestEnemy)< CastRange + 75*#allys )        --判断目标,判断英雄的位置是否安全
                then
                    return BOT_ACTION_DESIRE_LOW, WeakestEnemy;
                end
            end
            if (WeakestCreep~=nil)
            then
                if ( CanCast1( WeakestCreep )and GetUnitToUnitDistance(npcBot,WeakestCreep)< CastRange + 75*#allys )
                then
                    return BOT_ACTION_DESIRE_LOW, WeakestCreep;
                end
            end
        end
    end

(5) 通用
对模式决定的当前目标使用技能

-- If we're going after someone
    if ( npcBot:GetActiveMode() == BOT_MODE_ROAM or
         npcBot:GetActiveMode() == BOT_MODE_TEAM_ROAM or
         npcBot:GetActiveMode() == BOT_MODE_DEFEND_ALLY or
         npcBot:GetActiveMode() == BOT_MODE_ATTACK ) 
    then
        local npcTarget = npcBot:GetTarget();
        
        if(ManaPercentage>0.4)
        then
            if ( npcTarget ~= nil ) 
            then
                if ( CanCast1( npcTarget )  and GetUnitToUnitDistance(npcBot,npcTarget)< CastRange + 75*#allys)     --距离是不是足够近
                then
                    return BOT_ACTION_DESIRE_MODERATE, npcTarget;
                end
            end
        end
    end

(6)如果上面的部分没有获取到合适的目标,返回欲望0,和空单位,以防出现错误。

    return BOT_ACTION_DESIRE_NONE, 0;

2.宙斯2技能与之前类似的部分便不再详细指出。
(1)打断目标
本技能有打断功能,应打断敌方英雄的吟唱。

    -- Check for a channeling enemy
    for _,enemy in pairs( enemys )
    do
        if ( enemy:IsChanneling() and CanCast2( enemy ))
        then
            return BOT_ACTION_DESIRE_HIGH, enemy,"target";      --由于本技能有多种目标类型,可以返回类型是什么。
        end
    end

(2)击杀附近可以击杀的敌人

    -- Kill enemy
    if(npcBot:GetActiveMode() ~= BOT_MODE_RETREAT )
    then
        if (WeakestEnemy~=nil)
        then
            if(HeroHealth<=WeakestEnemy:GetActualIncomingDamage(Damage,DAMAGE_TYPE_MAGICAL))        --伤害是否足够
            then
                if ( CanCast2( WeakestEnemy ) )
                then
                    return BOT_ACTION_DESIRE_HIGH,WeakestEnemy,"target";
                end
            end
        end
    end

(3)保护自己,消耗靠近英雄的敌人

    --protect myself
    local enemys2 = npcBot:GetNearbyHeroes( 500, true, BOT_MODE_NONE );     --获取500范围内的敌人
    if(npcBot:WasRecentlyDamagedByAnyHero(5))       --最近是否被敌人伤害
    then
        for _,enemy in pairs( enemys2 )
        do
            if ( CanCast2( enemy ) )
            then
                return BOT_ACTION_DESIRE_HIGH, enemy,"target"
            end
        end
    end

(4)消耗敌方英雄

    if(npcBot:GetActiveMode() ~= BOT_MODE_RETREAT )
        if(ManaPercentage>0.4 and ability:GetLevel()>=2 )       --蓝量大于40%,技能大于或等于2级
        then
            if (WeakestEnemy~=nil and CanCast2( WeakestEnemy ))
            then
                return BOT_ACTION_DESIRE_LOW,WeakestEnemy,"target";
            end
        end
    end

3.宙斯大招
检测所有敌方英雄的血量,如果能够击杀,且附近没有友方英雄,那么则使用技能。详细代码略。

4.宙斯雷云
放在防御塔附近,或敌方英雄附近。

    local tower=nil
    
    if ( npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_TOP or
         npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_MID or
         npcBot:GetActiveMode() == BOT_MODE_PUSH_TOWER_BOT )
    then
        local tower=npcBot:GetNearbyTowers(1500,false)
    end
    
    if (npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_TOP or
        npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_MID or
        npcBot:GetActiveMode() == BOT_MODE_DEFEND_TOWER_BOT ) 
    then
        local tower=npcBot:GetNearbyTowers(1000,true)
    end
    
    if(tower~=nil and GetUnitToUnitDistance(npcBot,tower[1])<CastRange+500)
    then
        return BOT_ACTION_DESIRE_LOW,tower[1]:GetLocation()
    end
    
    if (WeakestEnemy~=nil)
    then
        return BOT_ACTION_DESIRE_LOW,WeakestEnemy:GetLocation()
    end
    

判断技能能否对目标使用

其实这部分很简单,就是判断目标是否魔法免疫,是否无敌之类的,以防止英雄重复尝试对无效目标使用技能。

function CanCast1( npcTarget )
    return npcTarget:CanBeSeen() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable();
end

function CanCast2( npcTarget )
    return not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable();
end

function CanCast5( npcTarget )
    return npcTarget:IsHero() and not npcTarget:IsMagicImmune() and not npcTarget:IsInvulnerable();
end

至此,我们已经完成了一个英雄的技能使用设计,详细代码请参考附件。

参考通用函数

utility.GetWeakestUnit(EnemyUnits)函数

function GetWeakestUnit(EnemyUnits)
    
    if EnemyUnits==nil or #EnemyUnits==0 then
        return nil,10000;
    end
    
    local WeakestUnit=nil;
    local LowestHealth=10000;
    for _,unit in pairs(EnemyUnits) 
    do
        if unit~=nil and unit:IsAlive() 
        then
            if unit:GetHealth()<LowestHealth 
            then
                LowestHealth=unit:GetHealth();
                WeakestUnit=unit;
            end
        end
    end
    
    return WeakestUnit,LowestHealth
end

总结

总算写完了,真的是很久没有写这么长一篇文章了,希望大家能有所启发。在下一篇文章中,我会为大家介绍如何配置AI出装。

附件

所有代码已测试运行通过
1.示例代码