分享回合制策略游戏AI算法设计方法

原文:http://gamerboom.com/archives/45636

发表时间:2012-01-22 09:08:19

作者:Ed Welch


在动作类游戏中,AI对手总是拥有完美的灵敏度和快速反应能力等天然优势,所以这类游戏的AI设计挑战就是让AI更为人性化而非百战不败。

在回合制策略游戏中,情况就会有所改变,速度和准确度并非重要致胜因素,聪明的人类玩家总有办法轻松击败AI对手。实际上,我们根本不可能设计出能够打败骨灰级玩家的AI,但这并非问题的关键所在。

这里的AI设计挑战在于,让AI的攻击和防御策略显得机智而成熟,既要为玩家创造挑战,又要确保玩家最终总能获胜。假如玩家深谙AI策略的套路,那么他们很快就会感到无趣,所以必须为他们创造一些不可预期的挑战。

以典型的策略游戏为例

我们在此一款4X太空战斗游戏为例,玩家在游戏中的任务是扩大并统治银河系。每名玩家都有太空战舰,殖民战舰和自己的家园星球,并能够殖民其他可居住的星球。

我们会先从最重要的任务开始,为其编写一个简单的AI算法,令其分配靠近某项资源(例如星球或战舰)的顺序。保卫拥有生产队列的星球是第一要务,因为它们最有价值。

其次是保卫没有生产队列的殖民地,然后就是进攻敌人的家园星球,殖民可居住的星球,攻击敌人战舰,随后修复破损的战舰,最后就是探索未知的领域。所以我们要先着眼于首要任务,查看是否有敌军试图染指自己的殖民地。

从上图中可以看出,敌人护卫舰X和Y正同时威胁AI的家园和殖民地。我们可以看到与敌军最近的驱逐舰,并令其攻击敌军护卫舰。但这个算法存在一些瑕疵。如果要先对付Y,就得让与其距离最近的驱逐般A采取攻击行动。而当护卫舰X靠近时,我们就只剩下一个驱逐舰B可用,但B与其距离过远,无法近身攻击X,结果就让X成功轰炸了我们的家园星球。很显然,我们应该给B分配攻击Y的任务,让A去解决X。

这种简单算法还会产生其他问题,让我们看另一种更复杂的场景:

在这种新场景中,我们的驱逐舰A在上次攻击中遭遇重创,无法再有效发挥战斗力。最明智的做法就令其返回基地进行修复。那么我们就只剩下驱逐舰C和B可以保卫殖民地。但驱逐舰C因距离过远,无法及时靠近敌军护卫舰Y,它的位置更适合攻击敌人的殖民地。与此同时,AI殖民者已经全副武装,可以从其主要的殖民任务中脱身。

任务分值

为了解决上述问题,我们首先得设计一个计分系统。为每个任务分配以下的一般优先顺序:

保卫殖民地1
攻击敌人殖民地2
殖民星球3
攻击敌舰4
维修受损的战舰5
探索未知领域6

每个任务都有一个优先顺序调节器,例如,保卫殖民地的任务会根据殖民地的价值而调节其优先顺序(拥有生产队列的殖民地价值最高)。与此类似,维修任务的优先顺序也可根据战舰受损的严重程度进行调整,殖民任务则根据星球“可居住程度”分出优先顺序。

最后还要考虑负责执行任务的战舰距离:

任务分值 = (6 - 一般优先顺序 + 调节分值) / 战舰与任务目标的距离

因此在前面的场景中,虽然防御任务优先顺序更靠前,但驱逐舰C进攻敌人殖民地的任务分值更高,因为它与敌人大本营的距离更近。

除此之外,驱逐舰A因为严重受损,其维修调节分值也会更高,再加上它与维修队列的距离更近,所以它的维修任务分值明显高于防御任务。

算法大纲

整个算法可划分为4个步骤:

收集任务

AI会根据其传感范围列出一个敌军战舰和星球等目标,以及自己的资产列表。其任务内容如下:

当前目标对应任务
靠近我方殖民地的敌舰保卫殖民地
敌军战舰攻击敌军
敌人殖民地攻击敌方星球
可居住的星球殖民星球
破损的战舰维修战舰
未知领域探索未知领域

潜在任务

另一个问题就是,假如我们以错误的顺序分配任务,可能就无法达到最佳资源利用效果。我们可以通过分阶段分配任务解决问题。可以使用两个不同的分类:潜在任务(PossibleAssignment)、任务(Task)。前者将为潜在的“任务执行者”分配一个任务,并存储“任务分值”。任务中还要存储优先顺序、顺序调节器和目标。

现在我们要为每个“任务执行者”分配一个潜在任务目标,但必须注意排除不可行的组合。例如,我们不能让没有武器的战舰执行攻击任务,也不能让它在缺乏足够燃料的情况下执行某些任务。算法代码如下:

// listAsset contains a list of all assets (for instance ships)
for (n = 0; n < listTask.size(); n++) {
    for(f = 0; f < listAsset.size(); f++) {
        if (listAsset[f].isTaskSuitable(listTask[n])) {
            listPossAssignment.add(new PossibleAssignment(listTaskn]));
        }
    }
}

下一步我们就要计算每个潜在任务的分配分值,从最高分到最低分罗列出优先顺序。最后,我们就能完成任务分配。任务分配完成之后,就要给任务执行者标注“忙碌”状态,给该任务打上“已分配”标注,从而避免重复分配任务的情况。

以下是这种操作的部分代码:

for (n = 0; n < listPossAssignment.size();n++) {
    listPossAssignment[n].assign();
}

public void PossibleAssignment::assign() {
    if (task.isAssigned()) return;
    possibleTaskDoer.assign(this);
}

public void Ship::assign(PossibleAssignment possAssign) {
    if (task != null) return;
    task = possAssign.getTask();
    possAssign.getTask().assign(this);
}

重用针对星球生产任务的算法

如果还有剩余任务未分配,AI就得制造新的星际飞船进行补充。例如,我们已经发现有敌舰来袭,但却没有空余的战舰可击退敌人,所以我们就得制造新战舰来填补这个空缺。与之类似,如果我们探索到了一个可居住的星球,但手头上却没有空闲的殖民者可供调遣,就需要创建新的殖民者。

生产队列的创建优先顺序分配与星际飞船的任务分配一样,从分类图表可以看出,各类飞船与星球都来源于SpaceObject,所以它们可以使用同样的算法,仅需一些微小的调整即可。

简化操作:抛弃旧任务

因为这是一款回合制游戏,每一个新回合开始之时,上一回合的分配的任务就要作废。例如,你的驱逐舰打算攻击的敌方护卫舰可能突然中途撤退,或者你发现自己打算殖民的星球已经被敌人捷足先登了,这样你就不得不临时改变策略。

最简单的方法就是在每一回合开始时,抛弃所有的旧任务,重新分配资源。虽然这种做法看似缺乏效率,因为并非所有的任务都需要调整,但它确实可以简化AI代码,这样你就无需维持前几个回合的任务。

对AI算法来说,保持代码的简洁性尤其重要,因为这些代码总是很容易就变得极为复杂,增加调试和维护的难度。另外,我们必须在最后阶段,即运算彻底完工时才能进行最优化任务分配。

中途意外状况

在某个回合中,我们的战舰有可能发现新的敌人殖民地或者敌舰。这时我们就要给自己的战舰指派新的攻击任务,但如果这些战舰已经有任务在身,这就会造成其他问题。最简单的的解决方法就是再次运行资源分配路径,这样才能有效保证实现最优化的资源分配。

实际运用情况

这种AI算法是为4X策略游戏而设计,从上述例子中我们可以看出其控制敌人战舰所体现出的智能特点。

战舰可能会出乎意料地改变策略,假如敌舰弹药耗尽,它就有可能突然撤离战场,重返基地填充弹药;如果它没有可供其重返基地的燃料,可能就会转向探索未知领域(这是它可执行的最后一个任务)。等到新战舰出炉的时候,整个舰队的任务顺序可能都会重新调整。有些战舰返回基地维修,让新战舰接手作战任务。

这种相当简单的算法不但容易执行和调试,而且还可以创造一个极具挑战性的AI对手。

虽然这种算法是为回合制策略游戏而设计,但经过适当调整后也应该可以为其他类型的策略游戏所用。


游戏邦注:原文发表于2007年7月27日,所涉事件及数据以当时为准。(本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转载,如需转载请联系:游戏邦)


Designing AI Algorithms For Turn-Based Strategy Games

by Ed Welch [Game Design]

In action games the AI opponent always has the natural advantage: perfect accuracy and lightning fast reflexes, so the challenge in designing the AI for those games is making it act more human and to be beatable.

In turn-based strategy games the tables are turned. Speed and accuracy are no longer important factors and the cunning and intuition of the human player will easily out match any AI opponent. In fact, it's nearly impossible to design a AI that can beat an experienced player, but that is not really the point anyway.

The challenge is to make the AI's attack and defense strategy to appear intelligent and thought out, providing a challenge but letting the player win in the end. Once the player has familiarized himself/herself with the tactics of the AI the game rapidly gets boring, so a certain amount of unpredictability is desirable.

Challenges Involved: Looking At A Typical Strategy Game

The AI design problem is easiest understood by taking a real life example, in this case we take a space based war game.

Our example is what's called a 4X game, where you must expand and dominate the galaxy. Each player has war ships and colony ships and starts with a home planet and can colonize habitable planets.

A first attempt at writing the AI would be a simple algorithm to assign orders to each resource (i.e. a planet, or ship), starting with the most important first. Defending planets with production queues has the highest priority, because they are the most valuable.

The next highest, is defending colonies without production queues, then attacking enemy home planets, then colonizing habitable planets, then attacking enemy ships, then repairing damaged ships and lastly exploring uncharted territory. So, we take the highest priority task first, and check for any enemy ships that are close to our colonies.

As you can see in the image above enemy frigates X & Y threaten both the AI's home world and colony. So, we find the closest warships and assign them to attack. You might see here the flaw here in our algorithm. If by chance, frigate Y is handled first, destroyer A will be assigned because it's the closest. Then, when Frigate X is processed, the only ship left to attack is destroyer B, which is too far away to reach it and Frigate X succeeds in bombing our home planet. It's obvious that Destroyer B should be assigned to Frigate Y and Destroyer A to Frigate X.

Also, other problems can occur with this simple algorithm. Have a look at a more complex scenario:

In this new scenario we have Destroyer A badly damaged from a previous attack. It would be a futile sacrifice sending it into battle again. It's wiser to send it back to the home planet for repair. So, that leaves Destroyers C and B to defend our colonies. But destroyer C is too far away to reach frigate Y in time and would be better served to bomb the enemy colony, seeing as it's so close (not to mention, that fuel conversation is important too). Meanwhile, the AI colonizer is armed, and could be diverted from its primary colonization mission.

The Solution: The Resource Assignment Algorithm

Assignment Scoring

In order to solve the problems detailed above, firstly we design a scoring system. Each task is assigned a general priority as follows:

Defending our colonies1
Attacking enemy colonies2
Colonizing planets3
Attacking enemy ships4
Repairing damaged ships5
Exploring uncharted territory6

Each task also has a priority modifier, for instance the defense task gets a modifier for the value of the colony (colonies with production queues get very high modifier). Likewise, the repair task gets a modifier depending on the amount of damage and the colonize task gets a modifier depending on the "habitability" of the planet.

Finally the distance of the assigned ship is taken into account, as follows:

assignment score = (6 – general priority + modifier) / distance to ship that is assigned

Therefore, in the previous scenario destroyer C would get a higher score for attacking the enemy colony, even though the defense task has a higher priority, just because it was so close to the enemy planet.

Also, the priority modifier of the repair task for Destroyer A is quite high because it's so badly damaged. Coupled with that it is close to a repair queue, and that means that it scores higher than the defense task.

Algorithm Outline

The overall algorithm is broken into 4 parts:

Gathering Tasks

The AI has a list of enemy ships and planets within sensor range, as well as a list of its own assets. Tasks that need to be done are generated as follows:

Object presentTask generated
Enemy ship near colonyDefend colony task
Enemy shipAttack ship task
Enemy colonyAttack planet task
Habitable planetColonize planet task
Damaged shipRepair ship task
Uncharted territoryExplore task
Possible Assignments

The other part of the problem is that if we assign tasks in the wrong order the resource utilization will not be optimal. This can be resolved by assigning the tasks in phases. We use two special classes to help us: PossibleAssignment and Task. PossibleAssignment links a potential "task doer" (e.g. a ship) with a task and stores the "assignment score". Task stores the priority, priority modifier and objective.

Let's just take a quick look at our class hierarchy to make things clearer:

We generate a PossibleAssignment object for each combination of "task doer" to task. However, we eliminate impossible combinations. For instance, an unarmed ship cannot carry out an attack task, nor can it do a task if it doesn't have enough fuel to reach it. This is how the code looks:

// listAsset contains a list of all assets (for instance ships)
for (n = 0; n < listTask.size(); n++) {
    for(f = 0; f < listAsset.size(); f++) {
        if (listAsset[f].isTaskSuitable(listTask[n])) {
            listPossAssignment.add(new PossibleAssignment(listTaskn]));
        }
    }
}

Next, we calculate assignment scoring for each PossibleAssignment and sort the list in order, highest scores first. And finally, in the last stage, we physically make the assignments. Because the list has been sorted, the most effective assignments occur first. Once an assignment is made the task doer is marked as busy and also the task is marked as assigned, preventing double assignments.

Here is part of the code:

for (n = 0; n < listPossAssignment.size();n++) {
    listPossAssignment[n].assign();
}

public void PossibleAssignment::assign() {
    if (task.isAssigned()) return;
    possibleTaskDoer.assign(this);
}

public void Ship::assign(PossibleAssignment possAssign) {
    if (task != null) return;
    task = possAssign.getTask();
    possAssign.getTask().assign(this);
}
Reusing The Algorithm For Planet Production Assignment

The AI should manufacture new star ships if there are any leftover tasks that couldn't be taken care of by the existing fleet. For example, if we have spotted an enemy ship and there are no available warships to attack, then we need to build a new warship. Similarly, if there is a habitable planet and no available colonizers, then we need to build a new colonizer.

In fact, the build priorities for production queues are exactly the same as the task priorities for star ships. As you can see in the class diagram, both the classes Ship and Planet are derived from SpaceObject, so they both can be used in the same algorithm with little modification. This is a good example of code reuse in object oriented design.

The below diagram shows this in action:

Keeping Things Simple: Discarding Old Tasks

As this is a turn based game, at the start of each new turn all the tasks from the last turn become out-of-date. For instance, that enemy frigate that your destroyer was about to attack could suddenly retreat, or you could discover – to your horror – that the planet you were about to colonize has already been occupied by the enemy.

The easiest thing to do is just discard all tasks and call the resource assignment routine at the start of each turn. This may seem inefficient, because not all tasks need to be updated, however it does makes the AI code considerably less complicated, as you don't need to maintain tasks from previous turns.

Keeping the code uncomplicated is especially important in the case of AI algorithms, because these have a tendency grow overly complex very quickly, making debugging and maintenance very difficult. As well as that, all optimization tasks should be done at the final stage, after the algorithm has been completely finished, and then only if there is real evidence that the algorithm is to slow in the first place.

Mid-turn Surprises

During the course of our turn one of our ships may discover a new enemy colony, or ship. We could just assign a new attack task to the ship, but that would cause problems if it already had an existing task that was important. Again, the simplest and most fool-proof thing to do is just run the resource allocation routine again, as this guarantees the most optimal resource assignment.

Conclusion: How Does The Algorithm Work In Real Life?

This AI algorithm was designed during the development of a 4x strategy game (as you may have guessed from the example). In practice one got the impression that there was some sort of real intelligence behind the control of the enemy fleet.

Ships would change tactics unexpectedly. If an enemy ship ran out of ammo, it would suddenly break off battle and go back to base to re-arm. If it didn't have enough fuel to make it to base, then it would try to explore uncharted territory (the only useful task left). As new ships came out of the shipyards, the orders could change for the whole fleet. Some ships would return for repair and leave the fresh warships take up the attack.

Basically the algorithm provides good "bang for buck" ratio, a fairly uncomplicated algorithm that's easy to implement and debug, but yet provides a challenging AI opponent.

Even though, the algorithm was designed for one specific type of game it should be easily adaptable to other types of strategy game. (source:gamasutra)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>