Hacking Bitsy to run AI PT. 2: Ghosts N Stuff

https://zachtheperson.itch.io/bitsyman
Click to play game

Welcome to the second part where I will be going over the actual AI / game programming, and how I got it to interface with Bitsy. If you haven't read the first part, you can find it here

Communicating between Bitsy and our Code.


This game uses the standard start/update function convention (named pStart(), and pUpdate() respectively in the code).

I lodged the pStart() function call right after load_game() in the startExportedGame() function. so that it is called first thing, but also after all the game data has been initialized. It's job is to simply set up our own game variables and objects.

Then I called the pUpdate() function in two places. Once at the end of pStart() in the form of a "setInterval" which is set to update every 300 miliseconds, and a special world.player.update() (more on this in a second) was forcefully stuffed into Bitsy's updateInput() function to update the player position every time a key is pressed.
  • Hindsight: If I were to do this again I would probably put the pUpdate() function inside Bitsy's updateAnimation() function to make things a little easier and just remove the "setInterval()" from pStart()
We then write our ghost and player objects with their own update methods that are called every time pUpdate() is called. These methods must contain the following equivalent code:
  • bitsyToGameSync(): custom method which uses function calls discussed in the first post in order to get things such as player / ghost position equal to their positions in Bitsy.
    • EX: this.x = player().x //sets objects current position to current x position within bitsy.
    • EX: this.x = getRoom().items[getIdxByName("red_ghost")].x //sets our JS objects x position to the position of the red ghost item in Bitsy
      • getIdxByName() is a custom function that would find the object ID matching the name in the "item," list, and then use that ID to search for the actual object index within the "getRoom().items[]" list.
  • subroutine()/other update code: the actual code to update positions and properties. Basically our game logic.
  • gameToBitsySync(): once again, using functions and global variables discussed in the first post to set the positions and properties of our newly updated objects in Bitsy.
    • EX: player().x = this.x //sets player's x position within Bitsy to JS objects x position.
NOTE:  In the actual code, I didn't use these exact methods, but I used the same structure. The player character is the only thing that can actually move from within Bitsy, so the ghosts don't need the bitsyToGameSync() method. Also my player class really only needed 2 lines to update the player position therefore, I didn't bother to break that out into it's own method.

All other functions can be written in global scope and called using the "JS from dialog," hack discussed in the first post.

This is basically it for the generic stuff. If you came here wanting to know how to run your own scripts, this is all there is to it. In short: setup variables, initiate update loop, within update loop get data from Bitsy, modify it, and then sync that data back to Bitsy. From here on out I am going to get into the specifics of...

AI and Finite State Machines

This is what the AI states of those dumb guards from your favorite stealth game might look like.

As the diagram above shows, finite state machines are probably how you think of AI working. One "State," can transition into other states based on certain conditions, which get run until it changes to another state, and so on.

In practice this tends to take the form of function pointers and methods. You start out with a function pointer called something like "current_state," or "subroutine," and always call that function pointer in the object.update() method.

The subroutine pointer starts off set to an initial "state," which is a method in the class. The method then does whatever that state should do (IE: walk forward, loudly talk about how much they hate the player, etc.) and then check if it needs to switch to another state (IE: has the guard been shot? Set guard.subroutine = alerted)

In JavaScript this will look something like the following:
class OurAI{
   
constructor(){
       
this.health = 100;
       
this.subroutine = this.lowerHealth;
   
}

   
update(){
       
this.subroutine();
   
}

   
lowerHealth(){
       
health--;

       
if (health <= 0){
           
this.subroutine = this.raiseHealth;
       
}
   
}

   
raiseHealth(){
       
health++;

       
if (health >= 100){
           
this.subroutine = this.lowerHealth;
       
}
   
}
}

This is a really simple and arbitrary example of an AI who has 2 states: raiseHealth() and lowerHealth(). It starts out lowering it's health, and when the health get's below 0, it switches states to raiseHealth(), and when it's health get's above 100 it switches back to lowerHealth().

The reason this works is because our main pUpdate() can call ourAIObj.update(), which calls whatever method is stored in the this.subroutine variable. It doesn't actually care what method is actually stored in it, is so when pUpdate() calls it, our current state runs.

In the actual game I had a parent Ghost class which was inherited by an inky, blinky, pinky, and clyde class. All the states were the same between them, except the individual ghosts each had their getTarget() method overridden to use their own logic. That way the generic ghost class could call getTarget() and each class had it's own implementation of this method based off of this video

Conclusion

That's it for now, as I've pretty much covered everything important about creating AI in Bitsy, however if anyone wants to know more about the specifics of how I did anything in the game, let me know.

Also if you make anything cool using it this knowledge link to it down below, tweet at me, or send me an email, I would love to see what you made!

Comments

Popular posts from this blog

Hacking Bitsy to run AI PT. 1: Getting to know Bitsy

I need to switch to Linux... BAD (rant)