Hacking Bitsy to run AI PT. 1: Getting to know Bitsy
|Click to play game|
I'm finishing out my last year of college and since I don't have any required courses left in my Comp Sci major, one of the classes I am taking is a game design course. It's an "Intro to game design," course, and as such it's focusing more on the "design," aspect, and less on technical skills. For example: Our last project was to make a non-digital version of a video game so we turned Mario Kart into a board game (which kicked major ass btw). It's a really fun course and I'm having a blast!
Anyways, our current project is to design our own game. Since the course is mainly a freshman course and a lot of the other students haven't had any programming classes yet we are limited to really basic game creation tools.
Enter Bitsy. Bitsy is a simple game engine and editor that allows you to make fun little games centered around conversations and picking up items. There's no scripting language, and only a few basic tools to make conditional statements within dialog. There are some mods you can apply to do some more complex things; however, It's supposed to be super basic. It's supposed to force you to focus more on level design and world building. It's supposed to give you limits to test your creativity.
So naturally I downed a bottle of rum and got it to run finite state machine AI.
Before I get into how I did this, here are some things that will really help you out if you want to do this yourself:
- Permanent Items hack: it's easier to manipulate items than it is sprites, so the more we can treat items like sprites the better.
- External Game Data hack: This allows us to just download the HTML5 game once, and simply download the "Game Data," when we make changes to a level. It would be really tedious to have to re-copy-and-paste our modified code back into our file every time we tweaked something in the editor.
- Also in order to use this hack you need to be able to run a simple local http server. There are plenty of ways to do this (google around), but the easiest by far that I found is to install Python and run "python -m http.server" from the command line. This allows you to access your game by going to the url "localhost:8000" in your browser.
- Run arbitrary JS from dialog hack: This just helps us call some JS functions when we want from within Bitsy.
Quick Overview (TL;DR)Here's the overview of the steps. It's actually pretty simple, but the rest of this explanation is going to go a little in depth, so here is a quick overview of the basics of what we are doing:
- Write our own generic JS functions to be run by Bitsy (game logic, AI, etc.). You can write these anywhere in the file.
- Manually place function calls inside Bitsy's source code so that our code gets run (actually really easy)
- Optionally call some of these functions using the "arbitrary JS hack," discussed above.
Getting to know Bitsy's insides
|If you haven't seen this video, you should totally watch it|
Bitsy is a really easy engine to modify because when you download your game as an HTML5 file, it comes with the entire engine, in plain text, with almost every function and variable declared in global scope.
Global variables:Here are some important global variable objects that we are going to be using. You can play around with these by typing these into the debug / developer console in your web browser while your game is running. Any modifications to these variables will show up in real-time:
- room: an object that acts like a list of all rooms in your game as sub objects. You can use "getRoom()," to get current room, or "getRoom('roomName')" to get room object by name.
Each room contains these sub-objects:
- endings: an array of all endings in the room
- exits: an array of all exits in the room
- id: room ID
- items: an array of references to items. It only stores the ID of the item as it is listed in the "item," object, as well as the X/Y position.
- If you want to add or remove an item from a room, all you have to do is push() or splice() an item from this array.
- name: room name string
- pal: current color palette being used for the room
- tilemap: a 2D array that stores references to the tiles ID in the "tile," object.
- walls: I don't know what this is used for. Might be legacy code, since as far as I can tell what is considered a wall or not is stored in the individual tiles data.
- item: an object which acts a list of all items used in the game. There is one master entry for every item, even if it is used multiple times. Item data is stored here.
- Searching for info about items often means you have to cross examine the two lists "item," and "getRoom().items" in order to get what you want.
- if you want to get an item by name, you will need to write your own function to search through all sub-objects in "item," for the ID that matches the name.
- If you want to get a specific item in the room, you can use "getItem(getRoom().id, x, y)" to get an item ID at that position. If you need more data, you will likely have to cross reference the acquired ID with it's entry in the "item," list.
- tile: object which acts as master list of all tiles that can be use in a rooms tilemap. Stores things like color palette as well as if the tile is a wall or not.
- You can either get a tile ID at a position by using "getRoom().tilemap[y][x]" or by using "getTile(x, y)." You can then take this ID and use it to find the reference in the master tile list.
- sprite: Object that acts as a list of all sprites. Sprites can only be used once, so their room, position, and other info is stored here meaning no need for cross referencing.
- We aren't going to be using this much, as with the permanent item hack there isn't much a sprite can do that an item can't do, but it's still good to know.
- player(): not technically a global variable, but this is how we get the player object. This is what is referred to as the "Avatar," in the Bitsy editor. Here we can modify player position, sprite, color, etc.
The functions listed in the last part are good for getting things from Bitsy, but if you actually want Bitsy to do things for you, then you need to understand a bit about the function calls of the Bitsy engine.
With Bitsy hacks, most of the code simply serves to inject custom code at run-time into the existing bitsy engine code and is actually duplicated from hack to hack. By digging through the code a little ourselves, we can bypass the need to write a hack and just insert the function calls ourselves.
We will only be covering a couple functions in these posts and only as they pertain to us, but if you want to dig through the code yourself, I would recommend using an editor that has a "jump to definition," feature (in VS code it's CTRL + Click on the function usage) and start from the startExportedGame() function.
The code is really well written and named, so if you want to dig deeper it's pretty easy to figure out what things do in the code just by looking at them.
The main functions we are going to be looking into are:
- startExportedGame(): this is the first thing that is run, and calls:
- attachCanvas(): this creates the canvas element for graphics
- load_game(): initiates parsing the game data from plain text to actual game data, and calls Bitsy's onReady() function.
- onReady(): Initiates title dialog, and uses "setInterval()," to call the "update()," function every 16 milliseconds.
- update(): This function is the "instant update," function and handles things like keyboard input and player movement which needs to be real-time. It also does a bunch of other stuff I'm not going to get into but is worth checking out.
- The important thing for us is that this function also calls the "updateAnimation()," function.
- updateAnimation(): this function is called every time a sprite's animation changes. This will be useful for later when we only want our ghosts to move on a set interval.