神码ai人工智能写作机器人_游戏AI:机器人反击!

神码ai人工智能写作机器人

The following is a short extract taken from our new book, HTML5 Games: Novice to Ninja, written by Earle Castledine. Access to the book is included with SitePoint Premium membership, or you can grab a copy in stores worldwide. You can check out a free sample of the first chapter here.

以下是摘自Earle Castledine撰写的新书HTML5 Games:Ninja的新手 这本书的访问权限包含在SitePoint Premium会员资格中,或者您可以在世界各地的商店中索取一份副本。 您可以在此处查看第一章免费样本

We have all the tools at our disposal now to make fantastically detailed worlds to explore and inhabit. Unfortunately, our co-inhabitants haven’t proved themselves to be very worthy opponents. They’re dumb: they show no emotion, no thought, no anima. We can instill these characteristics via graphics, animation, and above all, artificial intelligence (AI).

现在,我们拥有所有可用的工具,可以制作出令人难以置信的详细世界来探索和居住。 不幸的是,我们的同居者并没有证明自己是非常值得的对手。 他们很愚蠢:他们没有情感,没有思想,没有生气 。 我们可以通过图形,动画,尤其是人工智能(AI)来灌输这些特征。

Artificial intelligence is a huge and extremely complex field. Luckily, we can get impressive results even with a lot more artificial than intelligence. A couple of simple rules (combined with our old friend Math.random) can give a passable illusion of intention and thought. It doesn’t have to be overly realistic as long as it supports our game mechanics and is fun.

人工智能是一个巨大而极其复杂的领域。 幸运的是,即使是比人工智能更多的人为因素 ,我们也可以获得令人印象深刻的结果。 几个简单的规则(与我们的老朋友Math.random结合使用)可以给出意图和思想的通过幻想。 只要它支持我们的游戏机制并且很有趣,就不必过于现实。

Like collision detection, AI is often best when it’s not too good. Computer opponents are superhuman. They have the gift of omniscience and can comprehend the entire state of the world at every point in time. The poor old human player is only able to see what’s visible on the screen. They’re generally no match against a computer.

像碰撞检测一样,当AI 不太好时,它通常是最好的。 电脑的对手是超人的。 他们拥有无所不能的天赋,可以在每个时间点理解整个世界。 可怜的老人类玩家只能看到屏幕上可见的内容。 它们通常无法与计算机匹敌。

But we don’t let them know that! They’d feel bad, question the future of humanity, and not want to play our games. As game designers, it’s our job to balance and dictate the flow of our games so that they’re always fair, challenging, and surprising to the player.

但是我们不让他们知道! 他们会感到难过,质疑人类的未来,不想玩我们的游戏。 作为游戏设计师,平衡和支配我们的游戏流程是我们的工作,以便它们始终对玩家公平,具有挑战性和令人惊讶。

故意运动 (Intentional Movement)

Choosing how sprites move around in the game is great fun. The update function is your blank canvas, and you get godlike control over your entities. What’s not to like about that!

选择精灵在游戏中如何移动非常有趣。 update功能是您的空白画布,您可以对实体进行神似的控制。 那不喜欢什么!

The way an entity moves is determined by how much we alter its x and y position every frame (“move everything a tiny bit!”). So far, we’ve moved things mostly in straight lines with pos.x += speed * dt. Adding the speed (times the delta) causes the sprite to move to the right. Subtracting moves it to the left. Altering the y coordinate moves it up and down.

实体移动的方式取决于我们每帧改变其xy位置的数量(“一点一点地移动!”)。 到目前为止,我们已经使用pos.x += speed * dt直线移动了东西。 增加速度(乘以增量)会导致精灵向右移动。 减法将其向左移动。 更改y坐标可上下移动。

To make straight lines more fun, inject a bit of trigonometry. Using pos.y += Math.sin(t * 10) * 200 * dt, the sprite bobs up and down through a sine wave. t * 10 is the frequency of the wave. t is the time in seconds from our update system, so it’s always increasing linearly. Giving that to Math.sin produces a smooth sine wave. Changing the multiplier will alter the frequency: a lower number will oscillate faster. 200 is the amplitude of the waves.

为了使直线更有趣,请注入一些三角函数。 使用pos.y += Math.sin(t * 10) * 200 * dt ,子画面通过正弦波上下摆动。 t * 10是波的频率。 t是我们更新系统中的时间(以秒为单位),因此它总是线性增加。 将其Math.sin会产生平滑的正弦波。 改变倍频会改变频率:数字越小振荡越快。 200是波的振幅

You can combine waves to get even more interesting results. Say you added another sine wave to the y position: pos.y += Math.sin(t * 11) * 200 * dt. It’s almost exactly the same as the first, but the frequency is altered very slightly. Now, as the two waves reinforce and cancel each other out as they drift in and out of phase, the entity bobs up and down faster and slower. Shifting the frequency and amplitude a lot can give some interesting bouncing patterns. Alter the x position with Math.cos and you have circles.

您可以组合波浪以获得更有趣的结果。 假设您在y位置添加了另一个正弦波: pos.y += Math.sin(t * 11) * 200 * dt 。 它几乎与第一个完全相同,但是频率变化很小。 现在,由于这两个波在相位上移入和移出时相互增强和抵消,因此实体会越来越快地上下摆动。 大量改变频率和幅度会产生一些有趣的反弹模式。 使用Math.cos更改x位置,您将有一个圆圈。

The important aspect of this is that movements can be combined to make more complex-looking behaviors. They can move spasmodically, they can drift lazily. As we go through this chapter, they’ll be able to charge directly towards a player, or to run directly away. They’ll be able to traverse a maze. When you combine these skills (a bobbing motion used in conjunction with a charge-at-player), or sequence them (run away for two seconds, then bob up and down for one second) they can be sculpted into very lifelike beings.

重要的方面是,可以将动作组合起来以做出看起来更复杂的行为。 他们可以痉挛地移动,可以懒惰地漂移。 当我们阅读本章时,他们将能够直接向玩家充电或直接逃跑。 他们将能够穿越迷宫。 当您结合使用这些技能(将弹跳动作与玩家的冲刺动作结合使用)或排序时(先逃跑两秒钟,然后上下摆动一秒钟),即可将它们雕刻成栩栩如生的生物。

航点 (Waypoints)

We need to spice up these apathetic ghosts and bats, giving them something to live for. We’ll start with the concept of a “waypoint”. Waypoints are milestones or intermediate target locations that the entity will move towards. Once they arrive at the waypoint, they move on to the next, until they reach their destination. A carefully placed set of waypoints can provide the game character with a sense of purpose, and can be used to great effect in your level design.

我们需要给这些冷漠的幽灵和蝙蝠加些香料,给它们一些生存的空间。 我们将从“航路点”的概念开始。 航点是实体将要到达的里程碑或中间目标位置。 一旦到达航路点,便继续前进到下一个路点,直到到达目的地。 精心放置的一组航点可以为游戏角色提供一种目标感,并可以在关卡设计中发挥巨大作用。

The waypoint-following bombs of Franco Ponticelli’s FlyMaze

So that we can concentrate on the concepts behind waypoints, we’ll introduce a flying bad guy who’s not constrained by the maze walls. The scariest flying enemy is the mosquito (it’s the deadliest animal in the world, after humans). But not very spooky. We’ll go with “bat”.

为了使我们能够集中精力于航点背后的概念,我们将介绍一个不受迷宫墙约束的飞行坏蛋。 飞行中最可怕的敌人是蚊子(蚊子是仅次于人类的世界上最致命的动物)。 但不是很诡异 。 我们将使用“蝙蝠”。

Bats won’t be complex beasts; they’ll be unpredictable. They’ll simply have a single waypoint they fly towards. When they get there, they’ll pick a new waypoint. Later (when we traverse a maze) we’ll cover having multiple, structured waypoints. For now, bats waft from point to point, generally being a nuisance to the player.

蝙蝠不会是复杂的野兽。 他们将是不可预测的。 他们只会有一个飞向的航路点。 当他们到达那里时,他们会选择一个新的航路点。 稍后(当我们穿越迷宫时),我们将介绍具有多个结构化的航路点。 就目前而言,蝙蝠从一个点到另一个点飘荡,通常对玩家是个麻烦。

To create them, make a new entity based on a TileSprite, called Bat, in entities/Bat.js. The bats need some smarts to choose their desired waypoint. That might be a function that picks a random location anywhere on screen, but to make them a bit more formidable we’ll give them the findFreeSpot functions, so the waypoint will always be a walkable tile where the player might be traveling:

要创建它们,请在entities/Bat.js基于TileSprite创建一个名为Bat的新实体。 蝙蝠需要一些聪明才智来选择所需的航路点。 这可能是挑选在屏幕上任意位置的任意位置的功能,反而使他们更加强大一点,我们将给予他们findFreeSpot功能,所以航点永远是一个适宜步行的瓷砖,玩家可能会旅行:

const bats = this.add(new Container());
for (let i = 0; i < 5; i++) {
  bats.add(new Bat(() => map.findFreeSpot()))
}

We have a new Container for the bats, and we create five new ones. Each gets a reference to our waypoint-picking function. When called, it runs map.findFreeSpot and finds an empty cell in the maze. This becomes the bat’s new waypoint:

我们有一个新的蝙蝠Container ,并创建了五个新的蝙蝠。 每个人都可以参考我们的航点选择功能。 调用时,它将运行map.findFreeSpot并在迷宫中找到一个空单元格。 这成为蝙蝠的新航路点:

class Bat extends TileSprite {
  constructor(findWaypoint) {
    super(texture, 48, 48);
    this.findWaypoint = findWaypoint;
    this.waypoint = findWaypoint();
    ...
  }
}

Inside Bat.js we assign an initial goal location, then in the bat’s update method we move towards it. Once we’re close enough, we choose another location to act as the next waypoint:

Bat.js我们分配一个初始目标位置,然后在bat的update方法中向其移动。 足够接近后,我们选择另一个位置作为下一个航路点:

// Move in the direction of the path
const xo = waypoint.x - pos.x;
const yo = waypoint.y - pos.y;
const step = speed * dt;
const xIsClose = Math.abs(xo) <= step;
const yIsClose = Math.abs(yo) <= step;

How do we “move towards” something, and how do we know if we’re “close enough”? To answer both of these questions, we’ll first find the difference between the waypoint location and the bat. Subtracting the x and y values of the waypoint from the bat’s position gives us the distance on each axis. For each axis we define “close enough” to mean Math.abs(distance) <= step. Using step (which is based on speed) means that the faster we’re traveling, the further we need to be to be “close enough” (so we don’t overshoot forever).

我们如何“走向”某事物,以及我们如何知道自己是否“足够接近”? 要回答这两个问题,我们首先要找到航点位置和蝙蝠之间的区别。 从蝙蝠的位置减去航路点的xy值,便得出了每个轴上的距离。 对于每个轴,我们定义“足够接近”以表示Math.abs(distance) <= step 。 使用step (基于speed )意味着我们行进得越快,我们就需要离“足够近”(以便我们不会永远超调)。

Note: Take the absolute value of the distance, as it could be negative if we’re on the other side of the waypoint. We don’t care about direction, only distance.

注意:获取距离的绝对值,因为如果我们在航路点的另一侧,则它可能为负。 我们不在乎方向,只在乎距离。

if (!xIsClose) {
  pos.x += speed * (xo > 0 ? 1 : -1) * dt;
}
if (!yIsClose) {
  pos.y += speed * (yo > 0 ? 1 : -1) * dt;
}

To move in the direction of the waypoint, we’ll break movement into two sections. If we’re not too close in either the x or y directions, we move the entity towards the waypoint. If the ghost is above the waypoint (y > 0) we move it down, otherwise we move it up—and the same for the x axis. This doesn’t give us a straight line (that’s coming up when we start shooting at the player), but it does get us closer to the waypoint each frame.

为了朝着航路点的方向移动,我们将移动分为两部分。 如果我们在xy方向上都不太靠近,则将实体移向航路点。 如果重影位于航路点上方( y > 0 ),则将其向下移动,否则将其向上移动,并且与x轴相同。 这不会给我们一条直线(当我们开始向玩家射击时会出现一条直线),但是它确实使我们更接近每一帧的航路点。

if (xIsClose && yIsClose) {
  // New way point
  this.waypoint = this.findWaypoint();
}

Finally, if both horizontal and vertical distances are close enough, the bat has arrived at its destination and we reassign this.waypoint to a new location. Now the bats mindlessly roam the halls, as we might expect bats to do.

最后,如果水平距离和垂直距离都足够近,则表明蝙蝠已经到达目的地,我们将this.waypoint重新分配给新位置。 现在,蝙蝠像我们可能期望的那样,无意识地在大厅里漫游。

This is a very simple waypoint system. Generally, you’ll want a list of points that constitute a complete path. When the entity reaches the first waypoint, it’s pulled from the list and the next waypoint takes its place. We’ll do something very similar to this when we encounter path finding shortly.

这是一个非常简单的航点系统。 通常,您将需要构成完整路径的点列表。 当实体到达第一个航点时,它将从列表中拉出,下一个航点取而代之。 当我们很快遇到寻路时,我们将做与此非常相似的事情。

向目标移动并射击 (Moving, and Shooting, Towards a Target)

Think back to our first shoot-’em-up from Chapter 3. The bad guys simply flew from right to left, minding their own business—while we, the players, mowed down the mindless zombie pilots. To level the playing field and make things more interesting from a gameplay perspective, our foes should at least be able to fire projectiles at us. This gives the player an incentive to move around the screen, and a motive for destroying otherwise quite peaceful entities. Suddenly we’re the hero again.

回想一下第3章中的第一个射击游戏。坏家伙只是从右向左飞来飞去,注意他们自己的事,而我们这些球员则在嘲笑那些毫无头脑的僵尸飞行员。 为了使游戏环境平整并使游戏玩法更有趣,我们的敌人至少应该能够向我们发射弹丸 。 这给玩家提供了在屏幕上四处移动的动机,以及消灭原本相当和平的实体的动机。 突然我们又成为英雄了。

Providing awareness of the player’s location to bad guys is pretty easy: it’s just player.pos! But how do we use this information to send things hurtling in a particular direction? The answer is, of course, trigonometry!

向坏人提供玩家位置的认识非常容易:这只是player.pos ! 但是,我们如何使用这些信息将事物发送到特定的方向呢? 答案当然是三角函数!

function angle (a, b) {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  const angle = Math.atan2(dy, dx);

  return angle;
}

Note: In this chapter, we’ll see a couple of trigonometric functions for achieving our immediate goals of “better bad guys”—but we won’t really explore how they work. This is the topic of next chapter … so if you’re a bit rusty on math, you can breathe easy for the moment.

注意:在本章中,我们将看到几个三角函数,用于实现我们的“更好的坏人”的近期目标-但我们不会真正探讨它们的工作原理。 这是下一章的主题……因此,如果您对数学有些生疏,可以暂时放松一下。

In the same way we implemented math.distance, we first need to get the difference between the two points (dx and dy), and then we use the built-in arctangent math operator Math.atan2 to get the angle created between the two vectors. Notice that atan2 takes the y difference as the first parameter and x as the second. Add the angle function to utils/math.js.

以我们实现math.distance的相同方式,我们首先需要获取两个点( dxdy )之间的 ,然后使用内置的反正切数学运算符Math.atan2来获取两个向量之间创建的角度。 请注意, atan2y差作为第一个参数,将x用作第二个参数。 将angle函数添加到utils/math.js

Most of the time in our games, we’ll be looking for the angle between two entities (rather than points). So we’re usually interested in the angle between the center of the entities, not their top-left corners as defined by pos. We can also add an angle function to utils/entity.js, which first finds the two entities’ centers and then calls math.angle:

在我们的游戏中,大多数时候,我们都会寻找两个实体之间的夹角(而不是点)。 因此,我们通常对实体中心之间的角度感兴趣,而不是由pos定义的实体左上角。 我们还可以向utils/entity.js添加一个angle函数,该函数首先找到两个实体的中心, 然后调用math.angle

function angle(a, b) {
  return math.angle(center(a), center(b));
}

The angle function returns the angle between the two positions, in radians. Using this information we can now calculate the amounts we have to modify an entity’s x and y position to move in the correct direction:

angle函数以弧度返回两个位置之间的角度。 现在,使用这些信息,我们可以计算出修改实体的xy位置以朝正确方向移动的数量:

const angleToPlayer = entity.angle(player.pos, baddie.pos);
pos.x += Math.cos(angle) * speed * dt;
pos.y += Math.sin(angle) * speed * dt;

To use an angle in your game, remember that the cosine of an angle is how far along the x axis you need to move when moving one pixel in the angle direction. And the sine of an angle is how far along the y axis you need to move. Multiplying by a scalar (speed) number of pixels, the sprite moves in the correct direction.

要在游戏中使用角度,请记住,角度的余弦是在角度方向上移动一个像素时沿x轴需要移动的距离。 角度的正弦是您需要沿着y轴移动多远。 乘以标量( speed )像素数,子画面会朝正确的方向移动。

Knowing the angle between two things turns out to be mighty important in gamedev. Commit this equation to memory, as you’ll use it a lot. For example, we can now shoot directly at things—so let’s do that! Create a Bullet.js sprite to act as a projectile:

知道两件事之间的夹角在游戏开发中非常重要。 将该方程式存储到内存中,因为您会经常使用它。 例如,我们现在可以直接事物开枪-让我们开始吧! 创建一个Bullet.js精灵以充当弹丸:

class Bullet extends Sprite {
  constructor(dir, speed = 100) {
    super(texture);
    this.speed = speed;
    this.dir = dir;
    this.life = 3;
  }
}

A Bullet will be a small sprite that’s created with a position, a velocity (speed and direction), and a “life” (that’s defaulted to three seconds). When life gets to 0, the bullet will be set to dead … and we won’t end up with millions of bullets traveling towards infinity (exactly like our bullets from Chapter 3).

Bullet是一个小的精灵,它是由一个位置,一个速度(速度和方向)和一个“生命”(默认为三秒钟)创建的。 当生命变为零时,子弹将被设置为dead弹……而我们最终将不会获得数百万发向无限远的子弹(就像我们第3章中的子弹一样)。

update(dt) {
  const { pos, speed, dir } = this;

  // Move in the direction of the path
  pos.x += speed * dt * dir.x;
  pos.y += speed * dt * dir.y;

  if ((this.life -= dt) < 0) {
    this.dead = true;
  }
}

The difference from our Chapter 3 bullets is that they now move in the direction given when it was instantiated. Because x and y will represent the angle between two entities, the bullets will fire in a straight line towards the target—which will be us.

与我们的第3章项目符号的不同之处在于,它们现在沿实例化时给定的方向移动。 因为xy代表两个实体之间的角度,所以子弹将以直线向目标开火-这就是我们。

The bullets won’t just mysteriously appear out of thin air. Something needs to fire them. We need another new bad guy! We’ll deploy a couple of sentinels, in the form of top-hat totems. Totems are the guards of the dungeons who watch over the world from the center of the maze, destroying any treasure-stealing protagonists.

子弹不仅会神秘地凭空出现。 需要解雇他们。 我们需要另一个新的坏蛋! 我们将以礼帽图腾的形式部署几个哨兵。 图腾是地下城的守卫,他们从迷宫的中心监视着世界,摧毁了所有盗窃宝藏的主角。

Top-hat totems: Toptems.

The Totem.js entity generates Bullets and fires them towards the Player. So they need a reference to the player (they don’t know it’s a player, they just think of it as the target) and a function to call when it’s time to generate a bullet. We’ll call that onFire and pass it in from the GameScreen so the Totem doesn’t need to worry itself about Bullets:

Totem.js实体生成Bullets并将其发射到Player 。 因此,他们需要引用玩家(他们不知道它是玩家,他们只是将其视为target ),以及一个需要在生成子弹时调用的函数。 我们将称之为onFire ,并从在它传递GameScreen这样的Totem并不需要担心本身有关Bullets

class Totem extends TileSprite {
  constructor(target, onFire) {
    super(texture, 48, 48);
    this.target = target;
    this.onFire = onFire;
    this.fireIn = 0;
  }
}

When a new Totem is created, it’s assigned a target, and given a function to call when it shoots a Bullet. The function will add the bullet into the main game container so it can be checked for collisions. Now Bravedigger must avoid Bats and Bullets. We’ll rename the container to baddies because the collision logic is the same for both:

创建新的Totem ,会为其分配一个目标,并为其分配一个在发射Bullet时调用的功能。 该功能会将子弹添加到主游戏容器中,以便可以检查是否有碰撞。 现在,勇敢者必须避开Bats Bullets 。 我们将容器重命名为baddies因为两者的碰撞逻辑是相同的:

new Totem(player, bullet => baddies.add(bullet)))

To get an entity on screen, it needs to go inside a Container to be included in our scene graph. There are many ways we could do this. We could make our main GameScreen object a global variable and call gameScreen.add from anywhere. This would work, but it’s not good for information encapsulation. By passing in a function, we can specify only the abilities we want a Totem to perform. As always, it’s ultimately up to you.

要在屏幕上显示实体,需要将其放入Container以包含在场景图中。 我们有很多方法可以做到这一点。 我们可以使我们的主GameScreen对象成为全局变量, gameScreen.add从任何地方调用gameScreen.add 。 这可以工作,但是不利于信息封装。 通过传递函数,我们可以指定我们希望Totem执行的功能。 与往常一样,最终取决于您。

Warning: There’s a hidden gotcha in our Container logic. If we add an entity to a container during that container’s own update call, the entity will not be added! For example, if Totem was inside baddies and it tried to add a new bullet also to baddies, the bullet would not appear. Look at the code for Container and see if you can see why. We’ll address this issue in Chapter 9, in the section “Looping Over Arrays”.

警告:我们的Container逻辑中有一个隐藏的陷阱。 如果我们在该容器自身的update调用期间将一个实体添加到该容器,则不会添加该实体! 例如,如果Totem在里面baddies ,并试图还添加了一个新的子弹baddies ,会不会出现子弹。 查看Container的代码,看看是否可以理解原因。 我们将在第9章的“遍历数组”中解决此问题。

When should the totem fire at the player? Randomly, of course! When it’s time to shoot, the fireIn variable will be set to a countdown. While the countdown is happening, the totem has a small animation (switching between two frames). In game design, this is called telegraphing—a subtle visual indication to the player that they had better be on their toes. Without telegraphing, our totems would suddenly and randomly shoot at the player, even when they’re really close. They’d have no chance to dodge the bullets and would feel cheated and annoyed.

图腾何时应该向玩家射击? 当然是随机的! 在拍摄时, fireIn变量将设置为倒数计时。 在倒计时的过程中,图腾具有较小的动画(在两个帧之间切换)。 在游戏设计中,这称为“ 电报” -一种向玩家微妙的视觉指示 ,表明他们最好保持警惕。 如果不进行电报,我们的图腾就会突然随机地向玩家射击,即使它们确实很接近。 他们没有机会躲避子弹,会感到被欺骗和烦恼。

if (math.randOneIn(250)) {
  this.fireIn = 1;
}
if (this.fireIn > 0) {
  this.fireIn -= dt;
  // Telegraph to the player
  this.frame.x = [2, 4][Math.floor(t / 0.1) % 2];
  if (this.fireIn < 0) {
    this.fireAtTarget();
  }
}

There’s a one-in-250 chance every frame that the totem will fire. When this is true, a countdown begins for one second. Following the countdown, the fireAtTarget method will do the hard work of calculating the trajectory required for a projectile to strike a target:

图腾发射的每一帧都有250分之一的机会。 如果是这样,倒数计时将开始一秒钟。 倒数之后, fireAtTarget方法将进行艰苦的工作来计算弹丸击中目标所需的轨迹:

fireAtTarget() {
  const { target, onFire } = this;
  const totemPos = entity.center(this);
  const targetPos = entity.center(target);
  const angle = math.angle(targetPos, totemPos);
  ...
}

The first steps are to get the angle between the target and the totem using math.angle. We could use the helper entity.angle (which does the entity.center calls for us), but we also need the center position of the totem to properly set the starting position of the bullet:

第一步是使用math.angle获取目标和图腾之间的角度。 我们可以使用帮助器entity.angle (由entity.center调用我们),但是我们还需要图腾的中心位置来正确设置项目符号的起始位置:

const x = Math.cos(angle);
const y = Math.sin(angle);
const bullet = new Bullet({ x, y }, 300);
bullet.pos.x = totemPos.x - bullet.w / 2;
bullet.pos.y = totemPos.y - bullet.h / 2;

onFire(bullet);

Once we have the angle, we use cosine and sine to calculate the components of the direction. (Hmm, again: perhaps you’d like to make that into another math function that does it for you?) Then we create a new Bullet that will move in the correct direction.

一旦有了角度,就可以使用余弦和正弦来计算方向的分量。 (再次,嗯:也许您想把它变成另一个对您有用的数学函数?)然后我们创建一个新的Bullet ,它将沿正确的方向移动。

That suddenly makes maze traversal quite challenging! You should spend some time playing around with the “shoot-at” code: change the random interval chance, or make it a timer that fires consistently every couple of seconds … or a bullet-hell spawner that fires a volley of bullets for a short period of time.

这突然使迷宫遍历变得非常具有挑战性! 您应该花一些时间来练习“射击”代码:更改随机间隔的机会,或者将其设置为每两秒钟持续触发一次的计时器……或者是会短时发射一连串子弹的子弹地狱生成器一段的时间。

Note: Throughout this book, we’ve seen many small mechanics that illustrate various concepts. Don’t forget that game mechanics are flexible. They can be reused and recombined with other mechanics, controls, or graphics to make even more game ideas—and game genres! For example, if you combine “mouse clicking” with “waypoints” and “fire towards”, we have a basic tower defense game! Create a waypoint path for enemies to follow: clicking the mouse adds a turret (that uses math.distance to find the closest enemy) and then fires toward it.

注意:在本书中,我们已经看到许多说明各种概念的小型机制。 不要忘记游戏机制很灵活。 它们可以重复使用,并与其他机制,控件或图形重新组合,以产生更多的游戏创意和游戏类型! 例如,如果您将“鼠标单击”与“航路点”和“朝…射击”结合使用,我们将提供基本的塔防游戏! 创建一个供敌人遵循的航路点路径:单击鼠标可添加一个炮塔(使用math.distance查找最接近的敌人),然后向其发射。

聪明的坏蛋:攻击和规避 (Smart Bad Guys: Attacking and Evading)

Our bad guys have one-track minds. They’re given a simple task (fly left while shooting randomly; shoot towards player …) and they do the same thing in perpetuity, like some mindless automata. But real bad guys aren’t like that: they scheme, they wander, they idle, they have various stages of alertness, they attack, they retreat, they stop for ice cream …

我们的坏蛋们一心一意。 给他们一个简单的任务(随机射击时向左飞;向玩家射击...),并且他们永久地做同样的事情,就像一些盲目的自动机一样。 但是真正的坏蛋不是那样的:他们计划,徘徊,闲置,处于各种戒备状态,攻击,撤退,停下来吃冰淇淋……

Skeletons at work—and rest—in Mozilla’s BrowserQuest

One way to model these desires is through a state machine. A state machine orchestrates behavior changes between a set number of states. Different events can cause a transition from the current state to a new state. States will be game-specific behaviors like “idle”, “walk”, “attack”, “stop for ice cream”. You can’t be attacking and stopping for ice cream. Implementing state machines can be as simple as storing a state variable that we restrict to one item out of a list. Here’s our initial list for possible bat states (defined in the Bat.js file):

为这些需求建模的一种方法是通过状态机状态机协调行为在一定数量的状态之间的变化。 不同的事件可能导致从当前状态到新状态的过渡状态将是特定于游戏的行为,例如“闲置”,“行走”,“攻击”,“停止吃冰淇淋”。 您不能攻击停下来喝冰淇淋。 实现状态机就像存储状态变量一样简单,我们将状态变量限制为列表中的一项。 这是我们可能的蝙蝠状态的初始列表(在Bat.js文件中定义):

const states = {
  ATTACK: 0,
  EVADE: 1,
  WANDER: 2
};

Note: It’s not necessary to define the states in an object like this. We could just use the strings “ATTACK”, “EVADE”, and “WANDER”. Using an object like this just lets us organize our thoughts—listing all the possible states in one place—and our tools can warn us if we’ve made an error (like assigning a state that doesn’t exist). Strings are fine though!

注意:不必在这样的对象中定义状态。 我们可以只使用字符串“ ATTACK”,“ EVADE”和“ WANDER”。 使用这样的对象只会让我们组织思想-在一个位置列出所有可能的状态-并且如果我们犯了错误(例如分配不存在的状态),我们的工具可以警告我们。 字符串很好!

At any time, a bat can be in only one of the ATTACK, EVADE, or WANDER states. Attacking will be flying at the player, evading is flying directly away from the player, and wandering is flitting around randomly. In the function constructor, we’ll assign the initial state of ATTACKing: this.state = state.ATTACK. Inside update we switch behavior based on the current state:

蝙蝠在任何时候都只能处于ATTACKEVADEWANDER状态之一。 攻击将在玩家身上进行 ,逃避直接在玩家附近飞行,并且徘徊在玩家周围。 在函数构造函数中,我们将分配ATTACK的初始状态: this.state = state.ATTACK 。 在update内部,我们根据当前状态切换行为:

const angle = entity.angle(target, this);
const distance = entity.distance(target, this);

if (state === states.ATTACK) {
  ...
} else if (state === states.EVADE) {
  ...
} else if (state === states.WANDER) {
  ...
}

Depending on the current state (and combined with the distance and angle to the player) a Bat can make decisions on how it should act. For example, if it’s attacking, it can move directly towards the player:

根据当前状态(并结合与玩家的距离和角度), Bat可以决定其行为方式。 例如,如果在进攻,​​它可以直接向玩家移动:

xo = Math.cos(angle) * speed * dt;
yo = Math.sin(angle) * speed * dt;
if (distance < 60) {
  this.state = states.EVADE;
}

But it turns out our bats are part chicken: when they get too close to their target (within 60 pixels), the state switches to state.EVADE. Evading works the same as attacking, but we negate the speed so they fly directly away from the player:

但是事实证明,我们的蝙蝠就像鸡一样:当它们离目标太近(60像素以内)时,状态切换为state.EVADE 。 躲避与攻击相同,但我们忽略了速度,因此它们直接飞离玩家:

xo = -Math.cos(angle) * speed * dt;
yo = -Math.sin(angle) * speed * dt;
if (distance > 120) {
  if (math.randOneIn(2)) {
    this.state = states.WANDER;
    this.waypoint = findFreeSpot();
  } else {
    this.state = states.ATTACK;
  }
}

While evading, the bat continually considers its next move. If it gets far enough away from the player to feel safe (120 pixels), it reassesses its situation. Perhaps it wants to attack again, or perhaps it wants to wander off towards a random waypoint.

避开时,蝙蝠不断考虑下一步行动。 如果距离播放器足够远,无法感到安全(120像素),它将重新评估其状况。 也许它想再次进攻,或者它想向随机的航路点走去。

When bats attack

Combining and sequencing behaviors in this way is the key to making believable and deep characters in your game. It can be even more interesting when the state machines of various entities are influenced by the state of other entities—leading to emergent behavior. This is when apparent characteristics of entities magically appear—even though you, as the programmer, didn’t specifically design them.

以这种方式组合和排序行为是在游戏中制作真实可信的角色的关键。 当各种实体的状态机受其他实体的状态影响而导致紧急行为时,可能会变得更加有趣。 这是当实体的明显特征神奇地出现的时候,即使您(作为程序员)并未专门设计它们。

Note: An example of this is in Minecraft. Animals are designed to EVADE after taking damage. If you attack a cow, it will run for its life (so hunting is more challenging for the player). Wolves in the game also have an ATTACK state (because they’re wolves). The unintended result of these state machines is that you can sometimes see wolves involved in a fast-paced sheep hunt! This behavior wasn’t explicitly added, but it emerged as a result of combining systems.

注意:在Minecraft中就是一个例子。 动物受伤害后可以逃避。 如果您攻击一头母牛,它将持续一生(因此,狩猎对玩家而言更具挑战性)。 游戏中的狼也具有攻击状态(因为它们是狼)。 这些状态机的意外结果是,您有时会看到狼参与快节奏的狩猎! 没有明确添加此行为,但是由于合并系统而出现。

更庄重的状态机 (A More Stately State Machine)

State machines are used a lot when orchestrating a game—not only in entity AI. They can control the timing of screens (such as “GET READY!” dialogs), set the pacing and rules for the game (such as managing cool-down times and counters) and are very helpful for breaking up any complex behavior into small, reusable pieces. (Functionality in different states can be shared by different types of entities.)

编排游戏时,不仅会在实体AI中使用状态机,还会使用很多状态机。 他们可以控制屏幕的显示时间(例如“准备就绪!”对话框),设置游戏的节奏和规则(例如管理冷却时间和计数器),并且对于将任何复杂的行为分解为细小,可重复使用的片段。 (处于不同状态的功能可以由不同类型的实体共享。)

Dealing with all of these states with independent variables and if … else clauses can become unwieldy. A more powerful approach is to abstract the state machine into its own class that can be reused and extended with additional functionality (like remembering what state we were in previously). This is going to be used across most games we make, so let’s create a new file for it called State.js and add it to the Pop library:

使用自变量处理所有这些状态, if … else条款可能变得笨拙。 一种更强大的方法是将状态机抽象到其自己的类中,该类可以通过其他功能重用和扩展(例如,记住我们之前所处的状态)。 这将在我们制作的大多数游戏中使用,因此让我们为其创建一个名为State.js的新文件,并将其添加到Pop库中:

class State {
  constructor(state) {
    this.set(state);
  }

  set(state) {
    this.last = this.state;
    this.state = state;
    this.time = 0;
    this.justSetState = true;
  }

  update(dt) {
    this.first = this.justSetState;
    this.justSetState = false;
    ...
  }
}

The State class will hold the current and previous states, as well as remember how long we’ve been in the current state. It can also tell us if it’s the first frame we’ve been in the current state. It does this via a flag (justSetState). Every frame, we have to update the state object (the same way we do with our MouseControls) so we can do timing calculations. Here we also set the first flag if it’s the first update. This is useful for performing state initialization tasks, such as reseting counters.

State类将保存当前和以前的状态,并记住我们进入当前状态已经有多长时间了。 它还可以告诉我们这是否是我们进入当前状态的第一帧。 它通过一个标志( justSetState )来实现。 在每一帧中,我们都必须更新state对象(使用MouseControls方法相同),以便可以进行时序计算。 如果它是第一次更新,我们还将在这里设置第first标志。 这对于执行状态初始化任务(例如重置计数器)很有用。

if (state.first) {
  // just entered this state!
  this.spawnEnemy();
}

When a state is set (via state.set("ATTACK")), the property first will be set to true. Subsequent updates will reset the flag to false. The delta time is also passed into update so we can track the amount of time the current state has been active. If it’s the first frame, we reset the time to 0; otherwise, we add dt:

当状态被设置(通过state.set("ATTACK")则属性first将被设置为true 。 随后的更新会将标志重置为false 。 增量时间也会传递给update因此我们可以跟踪当前状态处于活动状态的时间。 如果是第一帧,则将时间重置为0;否则,将时间重置为0。 否则,我们添加dt

this.time += this.first ? 0 : dt;

We now can retrofit our chase-evade-wander example to use the state machine, and remove our nest of ifs:

现在,我们可以改写我们的追逐逃逸示例以使用状态机,并删除if的嵌套:

switch (state.get()) {
  case states.ATTACK:
    break;
  case states.EVADE:
    break;
  case states.WANDER:
    break;
}
state.update(dt);

This is some nice documentation for the brain of our Bat—deciding what to do next given the current inputs. Because there’s a flag for the first frame of the state, there’s also now a nice place to add any initialization tasks. For example, when the Bat starts WANDERing, it needs to choose a new waypoint location:

对于Bat大脑来说 ,这是一些不错的文档-在当前输入下决定下一步该做什么。 因为状态的first帧有一个标记,所以现在还有个添加任何初始化任务的好地方。 例如,当Bat开始进行WANDER ,它需要选择一个新的航点位置:

case states.WANDER:
  if (state.first) {
    this.waypoint = findFreeSpot();
  }
  ...
  break;
}

It’s usually a good idea to do initialization tasks in the state.first frame, rather than when you transition out of the previous frame. For example, we could have set the waypoint as we did state.set("WANDER"). If state logic is self-contained, it’s easier to test. We could default a Bat to this.state = state.WANDER and know the waypoint will be set in the first frame of the update.

它通常是一个好主意,做初始化任务在state.first框架,而不是当你转换前一帧的出来 。 例如,我们可以像设置state.set("WANDER")一样设置路标。 如果状态逻辑是独立的,则测试会更容易。 我们可以将Bat 默认设置this.state = state.WANDER并且知道将在更新的第一帧中设置航路点。

There are a couple of other handy functions we’ll add to State.js for querying the current state:

我们将添加到State.js中的一些其他便捷函数来查询当前状态:

is(state) {
  return this.state === state;
}

isIn(...states) {
  return states.some(s => this.is(s));
}

Using these helper functions, we can conveniently find out if we’re in one or more states:

使用这些帮助器功能,我们可以方便地确定我们是否处于一种或多种状态:

if (state.isIn("EVADE", "WANDER")) {
  // Evading or wandering - but not attacking.
}

The states we choose for an entity can be as granular as needed. We might have states for “BORN” (when the entity is first created), “DYING” (when it’s hit, and stunned), and “DEAD” (when it’s all over), giving us discrete locations in our class to handle logic and animation code.

我们为实体选择的状态可以根据需要进行细化。 我们可能具有“ BORN”(首次创建实体时),“ DYING”(当其被击中并被击晕时)和“ DEAD”(当其结束时)的状态,为我们提供了在类中用于处理逻辑的离散位置和动画代码。

控制游戏流程 (Controlling Game Flow)

State machines are useful anywhere you need control over a flow of actions. One excellent application is to manage our high-level game state. When the dungeon game commences, the user shouldn’t be thrown into a hectic onslaught of monsters and bullets flying around out of nowhere. Instead, a friendly “GET READY” message appears, giving the player a couple of seconds to survey the situation and mentally prepare for the mayhem ahead.

需要控制动作流程的任何地方,状态机都非常有用。 一种出色的应用程序是管理我们的高级游戏状态。 当地牢游戏开始时,不应将用户扔进猛烈的猛烈攻击中,怪物和子弹无处不在。 取而代之的是,出现一条友好的“ READY READY”消息,让玩家有几秒钟的时间来调查情况并为未来的混乱做好心理准备。

A state machine can break the main logic in the GameScreen update into pieces such as “READY”, “PLAYING”, “GAMEOVER”. It makes it clearer how we should structure our code, and how the overall game will flow. It’s not necessary to handle everything in the update function; the switch statement can dispatch out to other methods. For example, all of the code for the “PLAYING” state could be grouped in an updatePlaying function:

状态机可以将GameScreen更新中的主要逻辑分解为“ READY”,“ PLAYING”,“ GAMEOVER”之类的内容。 它使我们更清楚如何构造代码以及整个游戏流程将变得更加清晰。 不需要处理update功能中的所有内容; switch语句可以调度到其他方法。 例如,可以将所有用于“ PLAYING”状态的代码归为一个updatePlaying函数:

switch(state.get()) {
  case "READY":
    if (state.first) {
      this.scoreText.text = "GET READY";
    }
    if (state.time > 2) {
      state.set("PLAYING");
    }
    break;

  case "PLAYING":
    if (entity.hit(player, bat)) {
      state.set("GAMEOVER");
    }
    break;

  case "GAMEOVER":
    if (controls.action) {
      state.set("READY");
    }
    break;
}
state.update(dt);

The GameScreen will start in the READY state, and display the message “GET READY”. After two seconds (state.time > 2) it transitions to “PLAYING” and the game is on. When the player is hit, the state moves to “GAMEOVER”, where we can wait until the space bar is pressed before starting over again.

GameScreen将以READY状态启动,并显示消息“ GET READY”。 两秒钟后( state.time > 2 ),它过渡到“ PLAYING”,游戏开始。 当玩家被击中时,状态会移至“ GAMEOVER”,在这里我们可以等到按下空格键再重新开始。

翻译自: https://www.sitepoint.com/game-ai-the-bots-strike-back/

神码ai人工智能写作机器人

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值