Ammo is a projectile created by towers with unique behaviour and designated attributes. Zap ammo's unique behaviour is that it can chain from the first targeted enemy to nearby enemies.
Zap ammo:
Before we can develop a solution to satisfy the above desired results, we must design it.
Ammo that recreates upon each collision
This design would have the tower fire at a rate such that the zap appeared a constant stream. Upon collision with an enemy, the zap would target another enemy and fire a zap toward it before destroying. This leverages much of the exisiting code base, but would appear inconsistent with zaps missing their inteded target and then chain taking visibly long to appear.
Ammo that does not move and collide, but provide an effect within a specified area
This ammo would completely replace the behaviour of the parent ammo, not moving but affecting enemies within its area for a set period of time. The ammo would have to be capable of targeting multiple enemies itself.
Let's continue with design 2. Once the ammo is created, it performs targeting.
To address these desired results, lets go one-by-one and break down each problem.
The design assume that the zap ammo is created with knowledge of the creating tower, or Source_Tower.
To satisfy these assumptions, either par_tower can be updated so that each ammo created now has these two new attributes, or obj_zap_tower can override the par_tower Step event.
Modify par_tower and par_ammo:
par_tower Step event:
ammo.Source_tower = self
par_ammo Create event:
// Tower which created ammo
Source_tower = noone
Desired Result: Targets multiple enemies with a chain effect starting at the tower limited by a maximum chain length
Design:
To be able to target multiple enemies, the zap ammo will have its own targeting behavior. The zap's design is not collision based, rather time limited within a specified area. Therefore, the targets may change throughout the zap's effect.
The first step is to target the initial enemy.
Since the targets may vary throughout the zap's effect, we will implement this behaviour in the Step event.
/// @description Select targets
Utilize built-in functions to select the nearest enemy.
Leveraging the built-in function instance_nearest, we can target the nearest enemy from the source tower's perspective.
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
Since instance_nearest can return noone, we need to check an enemy actually exists, otherwise the ammo should be destroyed.
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
#region Select next link
#endregion
}
else {
instance_destroy()
}
This first enemy however is still limited by the maximum chain length. Let's utilize ammo_range for this maximum chain length. Now in the Step event, we can ensure the enemy is within range.
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
if point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y) < ammo_range {
#region Select next link
#endregion
}
else {
instance_destroy()
}
}
else {
instance_destroy()
}
Step Event
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
if point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y) < ammo_range {
#region Select next link
#endregion
}
else {
instance_destroy()
}
}
else {
instance_destroy()
}
How would you determine the next enemy using the concepts below?
Concepts
We can start with every instance and narrow it down based on the following applicability criteria. The instance must:
This may result in several instances that match, therefore we will need criteria for picking the best match. This can be as simple as the first match, but to be more logically to the player, we'll use the closest match.
Utilizing iteration, we can check every instance on whether they meet the criteria. We do this through a loop.
We will be editing the Select next link region.
#region Select next link
#endregion
With the code block established, we can identify some steps. These will also be regions due to thier size.
#region Select next link
#region Populate list of applicable enemies
#endregion
#region Select best applicable enemy
#endregion
#endregion
Let's start by tackling the first step of populating a list. To implement the first downselect criteria, we can limit the iteration to only instances of the parent enemy.
#region Populate list of applicable enemies
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
// Ensure enemy not already in chain
}
#endregion
For the range criteria, we will need to calculate the range as it is variable with amount of links added. We can leverage the ammo_range to represent this maximum length.
#region Populate list of applicable enemies
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
}
}
#endregion
To know whether the enemy is already in the chain, we will need store which enemies are already linked. A list is best for this since the length can vary. The resulting chain of enemies will be used throughout the instance, so we'll declare at the instance level.
/// @description Declare attributes
// Maximum length of area effect define by stats
ammo_range = 0
// Chain of enemies targeted
chain_of_enemies = ds_list_create()
Data structures are created external to the instance and only referenced by id. Therefore it is important to destroy it upon instance destruction. We will use the Cleanup event for that.
/// @description Cleanup data structures
ds_list_destroy(chain_of_enemies)
Now with the instance-scope variable defined, we can leverage it in our step event. The design here will be to clear the list each iteration and repopulate as needed. This is to account for links that may be destroyed during its life. This occurs outside the region since populating applicable enemies is only one step in the downselect process. We can add First_Enemy as the first known instance in the chain.
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
#region Populate list of applicable enemies
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
}
}
#endregion
For ensuring the applicable enemy is not already in the chain, we can search chain_on_enemies for their id. We will use built-in function ds_list_find_index which returns -1 when the value does not exist.
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
#region Populate list of applicable enemies
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
}
}
}
#endregion
Enemies within range, where the range is less than the maximum chain length minus the length of all exisiting links, and not already in the chain, satisfy the criteria. Therefore, they can be added to list for further selection. Lets create a temporary ds_list for this outside of the region as it will be used for the next region.
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
#region Populate list of applicable enemies
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
ds_list_add(applicable_enemies, Enemy)
}
}
}
#endregion
With the temporary list of applicable enemies populated, we can select the best one.
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
#region Select next link
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
#region Populate list of applicable enemies
#endregion
#region Select best applicable enemy
#endregion
#endregion
This can be as simple as the first one in the list, but to make it appear more logical to the player, we can also do which one is closest.
#region Select best applicable enemy
// Min-max decision based on distance
#endregion
To implement a min-max on a certain attribute, we must store the current winner and their value to compare against. The Best_Enemy will be declared outside the region, since it is the output of the region's functionality.
// Best enemy to target next
var Best_Enemy = noone
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
#endregion
We know that no applicable enemy can be outside of the link range, so that's a good value to beat. Now to compare all enemies, we will once again utilize a loop, and when an enemy is better than the previous best, they will replace it.
// Best enemy to target next
var Best_Enemy = noone
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
// Iterate through all applicable enemies
for (var i=0; i<ds_list_size(applicable_enemies); i++) {
var Enemy = ds_list_find_value(applicable_enemies, i)
// Calculate value to compare
// Compare to best, replace if better
}
#endregion
With intent captured, we can add syntax.
// Best enemy to target next
var Best_Enemy = noone
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
// Iterate through all applicable enemies
for (var i=0; i<ds_list_size(applicable_enemies); i++) {
var Enemy = ds_list_find_value(applicable_enemies, i)
// Calculate value to compare
var enemy_distance = point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y)
// Compare to best, replace if better
if enemy_distance < best_distance {
// Replace best
Best_Enemy = Enemy
best_distance = enemy_distance
}
}
#endregion
With the closest enemy of all applicable enemies determined, we can save the enemy to our chain and clean up our temporary list.
// Best enemy to target next
var Best_Enemy = noone
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
// Iterate through all applicable enemies
for (var i=0; i<ds_list_size(applicable_enemies); i++) {
var Enemy = ds_list_find_value(applicable_enemies, i)
// Calculate value to compare
var enemy_distance = point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y)
// Compare to best, replace if better
if enemy_distance < best_distance {
// Replace best
Best_Enemy = Enemy
best_distance = enemy_distance
}
}
#endregion
// Add enemy to chain
ds_list_add(chain_of_enemies, Best_Enemy)
// Cleanup
ds_list_destroy(applicable_enemies)
Step Event
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
if point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y) < ammo_range {
#region Select next link
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
// Best enemy to target next
var Best_Enemy = noone
#region Populate list of applicable enemies
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
ds_list_add(applicable_enemies, Enemy)
}
}
}
#endregion
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
// Iterate through all applicable enemies
for (var i=0; i<ds_list_size(applicable_enemies); i++) {
var Enemy = ds_list_find_value(applicable_enemies, i)
// Calculate value to compare
var enemy_distance = point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y)
// Compare to best, replace if better
if enemy_distance < best_distance {
// Replace best
Best_Enemy = Enemy
best_distance = enemy_distance
}
}
#endregion
// Add enemy to chain
ds_list_add(chain_of_enemies, Best_Enemy)
// Cleanup
ds_list_destroy(applicable_enemies)
#endregion
}
else {
instance_destroy()
}
}
else {
instance_destroy()
}
This now gets us the first next enemy in the chain, but how do we keep the chain going?
Concepts:
Again, multiple ways to achieve this. What we are looking for is the most robust solution.
A simple way is to copy and paste the code down until we have enough duplicates to likely cover the maximum chain of enemies.
Or we can put the above code blocks in a loop and iterate that loop until maximum chain length is met.
In the zap's Step event we are starting with the following code:
#region Select next link
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
// Best enemy to target next
var Best_Enemy = noone
#region Populate list of applicable enemies
#endregion
#region Select best applicable enemy
#endregion
// Add enemy to chain
ds_list_add(chain_of_enemies, Best_Enemy)
// Cleanup
ds_list_destroy(applicable_enemies)</code>
#endregion
This will determine the best enemy to target next, but Populate list of applicable enemies is hard-coded to be the next enemy after the first enemy. Let's start by pulling out link range from the region as this will be the input for determining applicable enemies.
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
#region Populate list of applicable enemies
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
ds_list_add(applicable_enemies, Enemy)
}
}
}
#endregion
First_Enemy also needs replacing since it is defined as the first enemy in the chain. Local variable Previous_Enemy can be defined for this, with an inital value of First_Enemy.
// Previous enemy in the chain
var Previous_Enemy = First_Enemy
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, Previous_Enemy.x, Previous_Enemy.y)
#region Populate list of applicable enemies
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, Previous_Enemy.x, Previous_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
ds_list_add(applicable_enemies, Enemy)
}
}
}
#endregion
With the Populate list of applicable enemies region no longer hard-coded, lets try to surrond both regions in a loop. The loop will have to iterate while there are still applicable enemies. Previous_Enemy and link_range are defined outside of this while loop with their initial values, and reset at the end. Typing out the intent, would look like:
#region Select next link
// Define initial values
// While applicable enemies exists
// Populate list of applicable enemies
// Select best applicable enemy
#endregion
We already have regions for Populate list of applicable enemies and Select best applicable enemy so just the while loop and it's criteria remain.
#region Select next link
// Define initial values
// Previous enemy in the chain
var Previous_Enemy = First_Enemy
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, Previous_Enemy.x, Previous_Enemy.y)
// While applicable enemies exists
// Populate list of applicable enemies
// Select best applicable enemy
#endregion
Lets do this by assuming applicable enemies exist at the start, and then checking that assumption each iteration. We will need to update our variable criteria each iteration, this is best done when checking the assumption.
#region Select next link
// Define initial values
// Previous enemy in the chain
var Previous_Enemy = First_Enemy
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, Previous_Enemy.x, Previous_Enemy.y)
// Assume applicable enemies exist at first
// While applicable enemies exists
// Populate list of applicable enemies
// Check assumption
// Select best applicable enemy
// Recalculate variable criteria
#endregion
Finally, with the syntax filled in:
#region Select next link
// Previous enemy in the chain
var Previous_Enemy = First_Enemy
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, Previous_Enemy.x, Previous_Enemy.y)
// Assume applicable enemies exist at first
var applicable_enemies_exist = true
// While applicable enemies exists
while applicable_enemies_exist {
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
// Best enemy to target next
var Best_Enemy = noone
#region Populate list of applicable enemies
#endregion
// Check assumption
if ds_list_clear(applicable_enemies) {
applicable_enemies_exist = false
}
else {
#region Select best applicable enemy
#endregion
// Recalculate variable criteria
link_range -= point_distance(Best_Enemy.x, Best_Enemy.y, Previous_Enemy.x, Previous_Enemy.y)
Previous_Enemy = Best_Enemy
}
// Cleanup
ds_list_destroy(applicable_enemies)
}
#endregion
Create Event
/// @description Declare attributes
// Chain of enemies targeted
chain_of_enemies = ds_list_create()
Cleanup Event
/// @description Cleanup data structures
ds_list_destroy(chain_of_enemies)
Step event
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
if point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y) < ammo_range {
#region Select next link
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
#region Select next link
// Previous enemy in the chain
var Previous_Enemy = First_Enemy
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, Previous_Enemy.x, Previous_Enemy.y)
// Assume applicable enemies exist at first
var applicable_enemies_exist = true
// While applicable enemies exists
while applicable_enemies_exist {
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
// Best enemy to target next
var Best_Enemy = noone
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
#region Populate list of applicable enemies
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
ds_list_add(applicable_enemies, Enemy)
}
}
}
// Check assumption
if ds_list_clear(applicable_enemies) {
applicable_enemies_exist = false
}
else {
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
// Iterate through all applicable enemies
for (var i=0; i<ds_list_size(applicable_enemies); i++) {
var Enemy = ds_list_find_value(applicable_enemies, i)
// Calculate value to compare
var enemy_distance = point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y)
// Compare to best, replace if better
if enemy_distance < best_distance {
// Replace best
Best_Enemy = Enemy
best_distance = enemy_distance
}
}
#endregion
// Add enemy to chain
ds_list_add(chain_of_enemies, Best_Enemy)
// Recalculate variable criteria
link_range -= point_distance(Best_Enemy.x, Best_Enemy.y, Previous_Enemy.x, Previous_Enemy.y)
Previous_Enemy = Best_Enemy
}
// Cleanup
ds_list_destroy(applicable_enemies)
}
#endregion
}
else {
instance_destroy()
}
}
else {
instance_destroy()
}
Another more clean option is the use of functions and recursion.
In work
/// @function populate_applicable_enemies(applicable_enemies, First_Enemy, remaining_chain_length)
/// @description Populate provided list with all the possible enemies to target
/// @param {Id.DsList} applicable_enemies List of applicable enemies to populate
/// @param {Id.Instance} Last_Enemy Last enemy in the chain to compare against
/// @param {Real} max_distance The maximum distance for the enemy to be within
/// @return {Any} Returns nothing
function populate_applicable_enemies(applicable_enemies, Last_Enemy, max_distance){
for (var i; i < instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
if point_distance(Enemy.x, Enemy.y, Last_Enemy.x, Last_Enemy.y) < max_distance {
// This enemy is applicable, but is it best?
ds_list_add(applicable_enemies, Enemy)
}
}
}
<br>
/// @function pick_best_enemy(applicable_enemies, Last_Enemy)
/// @description Returns enemy
/// @param {Id.DsList} applicable_enemies List of applicable enemies
/// @param {Id.Instance} Last_Enemy Last enemy in the chain to compare against
/// @param {Real} max_distance The maximum distance of which the best enemy should be less than
/// @return {Id.Instance} Returns the closest enemy in the list of applicable enemies
function pick_best_enemy(applicable_enemies, Last_Enemy, max_distance){
var Best_Enemy = noone
var best_distance = max_distance
for (var i; i < ds_list_size(applicable_enemies); i++){
var Enemy = ds_list_find_value(applicable_enemies, i)
var enemy_distance = point_distance(Enemy.x, Enemy.y, Last_Enemy.x, Last_Enemy.y)
if enemy_distance < best_distance {
Best_Enemy = Enemy
best_distance = enemy_distance
}
}
return Best_Enemy
}
<br>
var Last_Enemy = First_Enemy
var remaining_chain_length = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
//
chain_of_enemies = ds_list_create()
ds_list_add(chain_of_enemies, Enemy)
//
while (reamaining_chain_length > 0) {
var applicable_enemies = ds_list_create()
populate_applicable_enemies(applicable_enemies, First_Enemy, remaining_chain_length)
var Enemy = pick_best_enemy(applicable_enemies, First_Enemy, remaining_chain_length)
ds_list_destroy(applicable_enemies)
//
var remaining_chain_length = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
ds_list_add(chain_of_enemies, Enemy)
}</code>
</pre>
</details>
<details>
<summary>Method 3 - Recursion</summary>
<pre>
<code>
function link_enemy(chain_of_enemies, remaining_chain_length) {
var applicable_enemies = ds_list_create()
populate_applicable_enemies(applicable_enemies, First_Enemy, remaining_chain_length)
var Enemy = pick_best_enemy(applicable_enemies, First_Enemy)
ds_list_destroy(applicable_enemies)
//
var remaining_chain_length = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y)
ds_list_add(chain_of_enemies, Enemy)
//
if remaining_chain_length > 0 {
link_enemy(chain_of_enemies, remaining_chain_length)
}
}
Desired Result: Display chain to player for awareness
Concepts
All drawing must occur in the Draw event.
We can use primitive shapes instead of sprites to make the variable chain lengths easier.
Starting in the Draw event, lets define intent. GameMaker utilizes a draw engine which must first be configured.
/// @description Display chain
// Setup drawing engine
#region Draw links
#endregion
First, we'll configure the drawing engine to use white for the lines.
// Setup drawing engine
draw_set_color(c_white)
Now for drawing links, we can use draw_line, but need to know where each link goes. For this, we can leverage the chain_of_enemies list. We'll start by drawing a line from the tower to the first enemy. By defining and updating a Link_Start for each line, a chain can be formed.
#region Draw links
// Set link start to tower
var Link_Start = Source_Tower
// Iterate through all enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Draw line between enemy and link start
// Reset link start
}
#endregion
With syntax:
#region Draw links
// Set link start to tower
var Link_Start = Source_Tower
// Iterate through all enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Draw line between enemy and link start
draw_line(Link_Start.x, Link_Start.y, Enemy.x, Enemy.y)
// Reset link start
Link_Start = Enemy
}
#endregion
Draw Event
/// @description Display chain
// Setup drawing engine
draw_set_color(c_white)
#region Draw links
// Set link start to tower
var Link_Start = Source_Tower
// Iterate through all enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Draw line between enemy and link start
draw_line(Link_Start.x, Link_Start.y, Enemy.x, Enemy.y)
// Reset link start
Link_Start = Enemy
}
#endregion
Damages enemies at a constant rate
How can we damage each enemy in the chain?
Concepts:
One method is to iterate through the chain of enemies each step and damage them.
In order to damage the enemies, the chain of enemies must already exist. To achieve this, we will move the Select targets code into the Begin Step event, and perform effects in the Step event.
In the new step event, lets capture intent.
/// @description Apply effects
// Damage enemies
We can simply iterate through our predetermined chain of enemies in the Begin Step event to accomplish this. However, we should ensure the enemy still exists before applying damage. This is due to whats called a race condition, where some other effect could cause the enemy to destroy prior to us applying damage.
/// @description Apply effects
// Damage enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Ensure enemy still exists
if instance_exists(Enemy) {
Enemy.hp -= dmg
}
}
Begin Step
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
if point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y) < ammo_range {
#region Select next link
#endregion
}
}
else {
instance_destroy()
}
Step
/// @description Apply effects
// Damage enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Ensure enemy still exists
if instance_exists(Enemy) {
Enemy.hp -= dmg
}
}
Disappears after a fixed time
Concepts:
How can we use the Step event to check each condition?
Let's modify the Step event to check conditions
/// @description Apply effects
// Damage enemies
#region Disappear upon conditions
// When out of time
#endregion
For the time, we can pre-set a counter in the Create event, and count down each Step. We can leverage built-in function game_get_speed to be able to specify the value in human readable seconds multiplied by the steps per second.
/// @description Declare attributes
// Chain of enemies targeted
chain_of_enemies = ds_list_create()
// Time to live in steps
time_to_live = 2*game_get_speed(gamespeed_fps)
Now we can fill out the Step event
/// @description Apply effects
// Damage enemies
#region Disappear upon conditions
// When out of time
if time_to_live <= 0 {
instance_destroy()
}
else time_to_live--;
#endregion
Create Event
/// @description Declare attributes
// Chain of enemies targeted
chain_of_enemies = ds_list_create()
// Time to live in steps
time_to_live = 2*game_get_speed(gamespeed_fps)
Step Event
/// @description Apply effects
// Damage enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Ensure enemy still exists
if instance_exists(Enemy) {
Enemy.hp -= dmg
}
}
#region Disappear upon conditions
// When out of time
if time_to_live <= 0 {
instance_destroy()
}
else time_to_live--;
#endregion
How can we users timers instead of a counter?
In work
Create Event
alarm[0] = time_to_live
Alarm Event 0
instance_destroy()
Create Event
/// @description Declare attributes
// Chain of enemies targeted
chain_of_enemies = ds_list_create()
// Time to live in steps
time_to_live = 2*game_get_speed(gamespeed_fps)
Cleanup Event
/// @description Cleanup data structures
ds_list_destroy(chain_of_enemies)
Begin Step event
/// @description Select targets
// Target first enemy
var First_Enemy = instance_nearest(Source_Tower.x, Source_Tower.y, par_enemy)
// Ensure enemy actually exists
if instance_exists(First_Enemy){
if point_distance(Source_Tower.x, Source_Tower.y, First_Enemy.x, First_Enemy.y) < ammo_range {
#region Select next link
// Empty persistent list
ds_list_clear(chain_of_enemies)
// Add enemy to chain
ds_list_add(chain_of_enemies, First_Enemy)
#region Select next link
// Previous enemy in the chain
var Previous_Enemy = First_Enemy
// Calculate variable criteria
var link_range = ammo_range - point_distance(Source_Tower.x, Source_Tower.y, Previous_Enemy.x, Previous_Enemy.y)
// Assume applicable enemies exist at first
var applicable_enemies_exist = true
// While applicable enemies exists
while applicable_enemies_exist {
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
// Best enemy to target next
var Best_Enemy = noone
// List of applicable enemies to select from
var applicable_enemies = ds_list_create()
#region Populate list of applicable enemies
// Iterate through all enemies
for (var i=0; i<instance_number(par_enemy); i++) {
var Enemy = instance_find(par_enemy, i)
// Ensure enemy is within range
if point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y) < link_range {
// Ensure enemy not already in chain
if ds_list_find_index(chain_of_enemies, Enemy) == -1 {
// Enemy is applicable
ds_list_add(applicable_enemies, Enemy)
}
}
}
// Check assumption
if ds_list_clear(applicable_enemies) {
applicable_enemies_exist = false
}
else {
#region Select best applicable enemy
// Min-max decision based on distance
var best_distance = link_range
// Iterate through all applicable enemies
for (var i=0; i<ds_list_size(applicable_enemies); i++) {
var Enemy = ds_list_find_value(applicable_enemies, i)
// Calculate value to compare
var enemy_distance = point_distance(Enemy.x, Enemy.y, First_Enemy.x, First_Enemy.y)
// Compare to best, replace if better
if enemy_distance < best_distance {
// Replace best
Best_Enemy = Enemy
best_distance = enemy_distance
}
}
#endregion
// Add enemy to chain
ds_list_add(chain_of_enemies, Best_Enemy)
// Recalculate variable criteria
link_range -= point_distance(Best_Enemy.x, Best_Enemy.y, Previous_Enemy.x, Previous_Enemy.y)
Previous_Enemy = Best_Enemy
}
// Cleanup
ds_list_destroy(applicable_enemies)
}
#endregion
}
else {
instance_destroy()
}
}
else {
instance_destroy()
}
Step Event
/// @description Apply effects
// Damage enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Ensure enemy still exists
if instance_exists(Enemy) {
Enemy.hp -= dmg
}
}
#region Disappear upon conditions
// When out of time
if time_to_live <= 0 {
instance_destroy()
}
else time_to_live--;
#endregion
Draw Event
/// @description Display chain
// Setup drawing engine
draw_set_color(c_white)
#region Draw links
// Set link start to tower
var Link_Start = Source_Tower
// Iterate through all enemies
for (var i=0; i<ds_list_size(chain_of_enemies); i++) {
var Enemy = ds_list_find_value(chain_of_enemies, i)
// Draw line between enemy and link start
draw_line(Link_Start.x, Link_Start.y, Enemy.x, Enemy.y)
// Reset link start
Link_Start = Enemy
}
#endregion