Dota2 AI 简易开发教程(一)——选择阵容及技能使用

在[第一篇教程](https://www.adamqqq.com/ai/dota2-ai-devlopment-tutorial.html "第一篇教程")中,我们主要介绍了Dota2 AI的基本情况,在这篇文章中,我们将介绍如何为AI选择阵容和技能使用(以宙斯为例)。

选择阵容

在[官方开发者维基](https://developer.valvesoftware.com/wiki/Dota_Bot_Scripting "官方开发者维基")中有着这样的说明:

如果你想控制英雄选择和分路,你可以在文件名为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就会选择自定义阵容了。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    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()是主函数,游戏选择阵容时就会调用这个函数,然后判断是天辉方还是夜魇方,之后分别选择各自的阵容。那么我们如何找到英雄对应的名字呢?在[这里](http://dota2-zh.gamepedia.com/作弊指令 "这里")便可找到英雄的实际名称。

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

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

1
2
3
4
5
6
7
8
9
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,可以用作数组)保存可以选择的英雄列表,然后随机出一个序号,选出表中的英雄。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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++端调用,所以需要返回一个分路情况的表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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楼上路。当然,如果要根据英雄的特性分路,那么需要加入更多判断。如果没有自己写好分路系统的话,可以暂时启用默认分路系统,即不编写这个函数。

技能加点

在[官方开发者维基](https://developer.valvesoftware.com/wiki/Dota_Bot_Scripting "官方开发者维基")中有着这样的说明:

如果你只想重写在技能和物品使用时的决策,你可以在文件名为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\ pc\ pc_heroes.txt中获取。 例如宙斯的技能为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	--以下为宙斯的技能名称
	\"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\",

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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],
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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中获取技能天赋名称也有点麻烦,那么有什么更简便的方法呢? 当然有,比如通过遍历英雄的技能槽来获取每一个技能和天赋的名称。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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,我们可以看到如下代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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()记录此英雄的句柄,方便后面的调用。 之后需要判断英雄能否使用技能,是正在施法,还是被沉默了。

1
2
3
4
	if ( npcBot:IsUsingAbility() or npcBot:IsChanneling() or npcBot:IsSilenced() )
	then 
		return
	end

然后获取一些常用的量

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

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
	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];记录此技能的句柄。 然后需要判断这个技能能否使用,即魔法够不够,冷却有没有好。

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
	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) 团战使用 当英雄模式处于攻击时,判断蓝量是否足够,敌方单位数量是否足够,然后找到最脆弱的敌方单位或者英雄,判断它们能不能被技能作用,最后将这个单位的句柄返回给上层主思考函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
--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) 线上正补

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
	--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) 发育打钱

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 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) 推进防守

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	-- 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) 通用 对模式决定的当前目标使用技能

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
-- 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,和空单位,以防出现错误。

1
	return BOT_ACTION_DESIRE_NONE, 0;

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

1
2
3
4
5
6
7
8
	-- 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)击杀附近可以击杀的敌人

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
	-- 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)保护自己,消耗靠近英雄的敌人

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	--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)消耗敌方英雄

1
2
3
4
5
6
7
8
9
	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.宙斯雷云 放在防御塔附近,或敌方英雄附近。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	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
	

判断技能能否对目标使用

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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)函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.[示例代码](http://www-10026452.cossh.myqcloud.com/bots_2017_06_07.7z "示例代码.7z")

使用 Hugo 构建
主题 StackJimmy 设计
Avatar

adamqqq's blog

关注计算机科学的发展