Artificial Intelligence can be a significant addition to many different types of video games. Techniques of artificial intelligence make a more compelling experience for the player by simulating human-like behaviors in their computerized opponents.
Our goal is to create computer-controlled characters that will:
* act in a way that is neither completely predictable nor purely random
* be responsive/adaptive to the player and to the environment (such as an items/obstacles in the layout)
* present a level of challenge that is neither too easy nor too hard
In this article, we will discuss A.I. in the context of enemy movement in an airplane shoot-em-up game, with emphasis on the idea of "waypoints": reference points in space used for navigation.
The main idea is to create a Sprite object (small and invisible) that serves as a Waypoint; each will contain two instance variables: ID and NextID. Each Enemy sprite contains an instance variable called TargetID, which stores the ID of a Waypoint, and each Enemy's angle of motion is constantly updated so that it is moving towards the corresponding Waypoint. Once an Enemy reaches the desired Waypoint, the Enemy updates its TargetID to the value stored in the Waypoint's NextID variable, and the Enemy's journey continues. In the sections that follow, we discuss the implementation details, as well as introduce additional techniques (each of which is optional) to make the enemy movement seem more "realistic".
This article includes a downloadable Capx file that illustrates the ideas presented.
Initial Setup
Movement
First, we create a new project with window size and layout size of 800 by 600 pixels. Then, create:
* a Sprite object named "Player" with 8-Direction and Bound to Layout behaviors
* a Sprite object named "Enemy" with Bullet and Timer behaviors, and an instance variable called TargetID with initial value 1.
* a Sprite object named "EnemyMissile" with the Bullet and Destroy Outside Layout behaviors.
* a Sprite object named "Waypoint" with two instance variables: ID and NextID
Then create multiple instances of the Waypoint sprite in your layout and arrange them as desired. Change their instance variable ID values to the numbers 1, 2, 3,... in the order they should be visited by the enemies. For each Waypoint, change the value of NextID to one more than ID, except for the last Waypoint, whose NextID value should be set to 1 (assuming they will be travelling in a loop).
There are two events to keep move the enemies along the waypoints. First, for each Enemy you need to select the Waypoint whose ID is equal to the Enemy's TargetID value, and set the angle of motion towards that Waypoint (the angle can be calculated using the function angle(Enemy.X, Enemy.Y, Waypoint.X, Waypoint.Y)). Also, make sure that the "Set Angle" property of the Enemy's Bullet behavior is set to "No", and rotate the Enemy to face the Player (this will be important when the enemies spawn objects to shoot at the player). Second, if an enemy collides with a Waypoint and that Waypoint's ID is equal to the Enemy's TargetID, then the Enemy determines its next target by setting its TargetID value to equal the NextID value of the Waypoint.
Firing
We've set up the code so that enemies are facing the player; next, we would like them to fire at regular intervals. There are a number of way to achieve this result:
* We could set up the event: Every X Seconds -- Enemy Spawns EnemyMissile. This causes the unrealistic (and hence undesirable) result that every Enemy on the screen will fire at the Player at the exact same time.
* When new enemies are spawned, we start a Timer action on each to activate Regularly, say, 1.5 seconds, and use the On Timer condition for an Enemy to spawn an EnemyMissile. This is slightly better, since Enemies could fire at different times, but an attentive player will still spot a mechanical regularity in the firing patterns.
* A better option is to modify the previous approach, and set a Timer with a randomly calculated duration (say, random(1.0, 2.0)) to activate Once, and then after the On Timer condition activates, set another Timer in the same way (random duration, type Once). There are no patterns in the fire timing here, and thus it may feel more realistic.
In addition, it may not be the case that all enemies have perfect aim, so to incorporate this feature you can calculate and set the missile's angle of motion to be the angle towards the player plus a small random value between -X and X. (For this to work correctly, EnemyMissile objects need to have the Bullet property "Set Angle" set to "No".)
Route Variations
After a while, players might see that all enemies appear to be moving along the same path (even though the path may be complicated). Surely, intelligent enemies would not act in such a way!
As our next improvement, we add a second instance variable to the Waypoint objects, called NextID2. The way we will use this variable is as follows: when an Enemy reaches its target Waypoint, we'll add a subevent with the condition to Compare Two Values: random(0.0 , 1.0) < 0.5 (a.k.a. "flip a coin"). If this is true, then the Enemy's new TargetID will be set to NextID, otherwise (using an Else condition), the new Target ID will be set to NextID2.
This can be accomplished using the events below; this event would then replace the previous event containing the condition Enemy On collision with Waypoint.
Also, for each Waypoint instance on our layout, we need to manually set the values of NextID2. For example, the Waypoint with ID=1 might set NextID=2 and NextID2=3. The Waypoint with ID=4 might set NextID=5 and NextID2=1.
Waypoint Movement
Again, after a while the player might notice that even though the enemies may follow different paths, the points at which enemies change direction are predictable.
To remedy this situation, we will also have make every Waypoint move as the Enemy sprites are moving toward them. This can be easily accomplished by adding the Sine behavior to Waypoints. The Sine behavior can change the value of a sprite's property (such as horizontal position, vertical position, size, opacity, etc.) in a periodic way. The type of value that is changing is displayed next to the "Movement" property in the properties panel; we'll leave the default setting of "Horizontal". The "Magnitude" property of the Sine behavior controls the amount of change; we'll leave this at the default value of 50.
To obtain more interesting Waypoint movement, we can add a second Sine behavior to the Waypoint object (which will appear under the behaviors heading as "Sine2". If we change the Movement to "Vertical" and change the "Period Offset" value to 1 (or in general, 1/4 the value of the period), this will cause the Waypoints to move in a circle.
((Technical mathy note: by changing the period offset of a Sine function to 1/4 the value of the period, we're actually creating a Cosine function, and in general, the equation of a circle can be parameterized by using one of Sine or Cosine for the X coordinate, and using the other for the Y coordinate.))
Enemy Speed
Another realistic touch we could add to Enemy movement is to vary the speed. Always moving at the same speed falls into the category of unrealistic patterns that we wish to avoid. Instead, we will set up our Enemy objects to alternate between slow and fast movement.
To accomplish this, we'll add a Sine behavior to the Enemy objects, and set the Movement property to "Value Only". Then, in the event sheet, we will have each Enemy set its Bullet speed to 150 + Enemy.Sine.Value, and with the Sine Magnitude set to 50, this means that the speed will fluctuate between 150 - 50 = 100 and 150 + 50 = 200.
Conclusion
In this article, we have seen a variety of ways to implement behaviors and events that could help the Enemy objects appear more realistic or intelligent than they actually are. There are probably many other improvements that could be made; feel free to share your ideas in the comments!