J
Jack the Person
Guest
I have been working on a procedurally generated dungeon crawler for the last 2 months, it works quite well apart from the AI. The game is based around a DS Grid which the enemies use to navigate by getting the direction to the player and moving to one of the 8 surrounding cells, the only problem is that this sort of AI is not very smart, as in they are not able to navigate around obstacles which also exist within the grid.
I did look into using MP Grids but decided not to as I couldn't figure out how I could get the game to calculate a new path after every cell as well as marking which cell was being moved to.
Anyway here's the coding I've got for the AI, feel free to take it:
Create Event:
Alarm 0:
Alarm 1:
Step (Where the movement code is located):
If you read through all that, I'd just like to point out that the User 0 event is just for particle creation and death detection which makes it irrelevant to the problem.
The collision grid is made up of 4 types of tiles: OccupiedTile, FreeTile, PlayerTile and EnemyTile, the player and enemies can only move around in the free tiles and the player and enemy tiles are just for attacking purposes and other than that act like an occupied tile to all other instances.
Any improvements or replacements to the AI will be highly appreciated. Thank you.
I did look into using MP Grids but decided not to as I couldn't figure out how I could get the game to calculate a new path after every cell as well as marking which cell was being moved to.
Anyway here's the coding I've got for the AI, feel free to take it:
Create Event:
Code:
///Set Variables
//Dynamic Values | Change on a per Enemy Basis
hitPoints = 13 //Health Points
defence = 0 //Defence
damage = 3 //Attack Damage
moveSpeed = 1.5 //Tiles moved per Second
attackSpeed = 2 //Attacks per Second
critChance = 1 //Critical Attack Chance as a Percentage
critMultiplyer = 10 //Critical Attack Damage Multiplyer
state = "Spawn" //Can be Changed | No Reason to do so Though...
alarm[0] = (irandom((room_speed/2)-1)+1) //Time Until Activation | Random Times should Lighten CPU Usage
image_speed = 0 //Basically for any Idle Animation | In Frame per Second
boss = false //Is the Enemy a Boss?
xp = 3 //How much XP the Player is Rewarded with for Killing this Enemy
//Static Values and Formulae | Doesn't Change Between Enemies | Don't Touch
moveSpeed = room_speed/moveSpeed //Converts Value from Tiles per Second to Pixels per Frame
attackSpeed = room_speed/attackSpeed //Converts Value from Attacks per Second to Frames per Attack
oldPoints = hitPoints //Vital in Damage Detection and Particle Creation
image_speed = image_speed/room_speed //Convert Value from FPS to Frames per CPU Cycle
xMove = 0 //Used in Horizontal Movement Calculation
yMove = 0 //Used in Vertical Movement Calculation
xNext = (x div CellArea) //The Enemy's next x Location on the Grid | Based off xCurrent and xMove
yNext = (y div CellArea) //The Enemy's next y Location on the Grid | Based off yCurrent and yMove
xCurrent = (x div CellArea) //The Enemy's Current x Location on the Grid
yCurrent = (y div CellArea) //The Enemy's Current y Location on the Grid
x = (xCurrent * CellArea) //Horizontal Grid Snapping at Spawn
y = (yCurrent * CellArea) //Vertical Grid Snapping at Spawn
ds_grid_set(global.collisionMap,xCurrent,yCurrent,EnemyTile) //Noting the Enemy's Location on the Collision Grid
attacking = false //Is the Enemy Attacking?
depth = -y //Just for Depth Ordering
Code:
///Delayed State Changes
if state = "Spawn" {state = "Wait"}
if state = "Moving"
{speed = 0
image_speed = 0
sprite_index = spr_enemyLeechIdle
x = xNext * CellArea
y = yNext * CellArea
xCurrent = (x div CellArea)
yCurrent = (y div CellArea)
xNext = xCurrent
yNext = yCurrent
xMove = 0
yMove = 0
state = "Wait"}
Code:
///Deal Damage
if state = "Attacking" //Checks the Function wasn't called by Accident
{if (global.collisionMap[# xCurrent+1, yCurrent] == PlayerTile) //Makes Sure the Player
or (global.collisionMap[# xCurrent-1, yCurrent] == PlayerTile) //is in a Position to
or (global.collisionMap[# xCurrent, yCurrent+1] == PlayerTile) //be Attacked
or (global.collisionMap[# xCurrent, yCurrent-1] == PlayerTile)
{if random(100) <= critChance //Generates a number between 0 & 100 | If Number is Smaller than critChance a Critical Hit is Made
{global.playerHealth -= round((damage*critMultiplyer)-((damage*critMultiplyer)*(global.defence * 0.075)/100))} //Critical Hit
else {global.playerHealth -= round(damage-(damage*(global.defence * 0.075)/100))} //Regular Attack
alarm[1] = attackSpeed} //Resets the Timer if the Player is Still in a Position to be Attacked
else {sprite_index = spr_enemyLeechIdle image_speed = 0 state = "Wait"}} //If the Player is not in Position then the Cycle is Reset
Code:
///State Actions
depth = -y
if state = "Wait" && instance_exists(obj_player)
{if (global.collisionMap[# xCurrent+1, yCurrent]) == PlayerTile
or (global.collisionMap[# xCurrent-1, yCurrent]) == PlayerTile
or (global.collisionMap[# xCurrent, yCurrent+1]) == PlayerTile
or (global.collisionMap[# xCurrent, yCurrent-1]) == PlayerTile then state = "Attack"
else {state = "Move"}}
else if state = "Move" && instance_exists(obj_player)
{var trueDirection = point_direction(x,y,obj_player.x,obj_player.y)
var roundDirection = round(trueDirection/45)*45
if roundDirection = 0 {xMove = 1 yMove = 0}
else if roundDirection = 45 {xMove = 1 yMove = -1}
else if roundDirection = 90 {xMove = 0 yMove = -1}
else if roundDirection = 135 {xMove = -1 yMove = -1}
else if roundDirection = 180 {xMove = -1 yMove = 0}
else if roundDirection = 225 {xMove = -1 yMove = 1}
else if roundDirection = 270 {xMove = 0 yMove = 1}
else if roundDirection = 315 {xMove = 1 yMove = 1}
else if roundDirection = 360 {xMove = 1 yMove = 0}
else {state = "Wait" xMove = 0 yMove = 0}
if (global.collisionMap[# (xCurrent + xMove), yCurrent] == FreeTile) {xNext = (xCurrent + xMove)} //Check new x-pos is valid
if (global.collisionMap[# xCurrent, (yCurrent + yMove)] == FreeTile) {yNext = (yCurrent + yMove)} //Check new y-pos is valid
if (global.collisionMap[# xNext, yNext] != FreeTile) && irandom(1) = 0 {xNext = xCurrent} else if (global.collisionMap[# xNext, yNext] != FreeTile) {yNext = yCurrent} //Makes sure new pos is correct
if (global.collisionMap[# (xCurrent + xMove), yCurrent] != FreeTile) && (global.collisionMap[# (xCurrent + xMove), yCurrent+1] == FreeTile) && (global.collisionMap[# xCurrent, yCurrent+1] == FreeTile) {yMove = 1}
else if (global.collisionMap[# (xCurrent + xMove), yCurrent] != FreeTile) && (global.collisionMap[# (xCurrent + xMove), yCurrent-1] == FreeTile) && (global.collisionMap[# xCurrent, yCurrent-1] == FreeTile) {yMove = -1}
else if (global.collisionMap[# xCurrent, (yCurrent + yMove)] != FreeTile) && (global.collisionMap[# xCurrent+1, (yCurrent + yMove)] == FreeTile) && (global.collisionMap[# xCurrent+1, yCurrent] == FreeTile) {xMove = 1}
else if (global.collisionMap[# xCurrent, (yCurrent + yMove)] != FreeTile) && (global.collisionMap[# xCurrent-1, (yCurrent + yMove)] == FreeTile) && (global.collisionMap[# xCurrent-1, yCurrent] == FreeTile) {xMove = -1}
if (global.collisionMap[# xNext, yNext] == FreeTile) && xCurrent != xNext or (global.collisionMap[# xNext, yNext] == FreeTile) && yCurrent != yNext
{ds_grid_set(global.collisionMap,xNext,yNext,EnemyTile) //Mark New Cell as Occupied
ds_grid_set(global.collisionMap,xCurrent,yCurrent,FreeTile) //Mark Old Cell as Free
move_towards_point(xNext*CellArea,yNext*CellArea,CellArea/moveSpeed) //Begin Moving to New Cell
if xNext != xCurrent && yNext != yCurrent then alarm[0] = sqrt(sqr(moveSpeed)+sqr(moveSpeed)) //Is the player moving straight or diagonally?
else alarm[0] = moveSpeed //Sets When to Stop Moving
state = "Moving"}
else state = "Wait"}
else if state = "Attack" && instance_exists(obj_player)
{ if (global.collisionMap[# xCurrent+1, yCurrent] == PlayerTile) {sprite_index = spr_enemyLeechBiteR image_speed = (18/room_speed) alarm[1] = attackSpeed state = "Attacking"} //(x/room_speed) produces a speed of x FPS
else if (global.collisionMap[# xCurrent-1, yCurrent] == PlayerTile) {sprite_index = spr_enemyLeechBiteL image_speed = (18/room_speed) alarm[1] = attackSpeed state = "Attacking"} //Three Bites / Second
else if (global.collisionMap[# xCurrent, yCurrent+1] == PlayerTile) {sprite_index = spr_enemyLeechBiteD image_speed = (18/room_speed) alarm[1] = attackSpeed state = "Attacking"} //One bite is 9 Frames
else if (global.collisionMap[# xCurrent, yCurrent-1] == PlayerTile) {sprite_index = spr_enemyLeechBiteU image_speed = (18/room_speed) alarm[1] = attackSpeed state = "Attacking"}
else {sprite_index = spr_enemyLeechIdle image_speed = 0 state = "Wait"}}
//Manage Health
if instance_exists(obj_player) {if state != "Move" && state != "Moving" {
if (global.collisionMap[# xCurrent+1, yCurrent] == PlayerTile) && obj_player.attackDirection = "Left" or
(global.collisionMap[# xCurrent, yCurrent+1] == PlayerTile) && obj_player.attackDirection = "Up" or
(global.collisionMap[# xCurrent-1, yCurrent] == PlayerTile) && obj_player.attackDirection = "Right" or
(global.collisionMap[# xCurrent, yCurrent-1] == PlayerTile) && obj_player.attackDirection = "Down"
{if mouse_check_button_pressed(mb_left)
{hitPoints -= round(global.swordDamage - (global.swordDamage * (defence * 0.075) /100))
if hitPoints >= 0 audio_play_sound(snd_hit,0.1,false)}}
if hitPoints != oldPoints {event_perform(ev_other,ev_user0)}}}
The collision grid is made up of 4 types of tiles: OccupiedTile, FreeTile, PlayerTile and EnemyTile, the player and enemies can only move around in the free tiles and the player and enemy tiles are just for attacking purposes and other than that act like an occupied tile to all other instances.
Any improvements or replacements to the AI will be highly appreciated. Thank you.