Skip to content

Instantly share code, notes, and snippets.

@steelx
Last active November 27, 2024 05:38
Show Gist options
  • Save steelx/c6a0847237254e9e02f2f77cf17914ac to your computer and use it in GitHub Desktop.
Save steelx/c6a0847237254e9e02f2f77cf17914ac to your computer and use it in GitHub Desktop.
GameMaker Scripts
/*
Watch demo at : youtube.com/@ajinkyax
The classic Behavior Tree for GameMaker 2024+
I customized this to be able to have 2 new methods such as Draw and DrawGUI which also allows to display current node status
@example
// in your DrawGUI event
var _gui_x = room_x_to_gui(x);
var _gui_y = room_y_to_gui(y-sprite_height);
bt_root.DrawGUI(_gui_x, _gui_y, false);
Forked from https://github.com/VitorEstevam/GML-Behaviour-Tree
*/
enum BTStates {
Running, // Task/sequence is still executing (in progress)
Success, // Task/sequence completed successfully
Failure, // Task/sequence failed to complete
Off // Task/sequence is inactive
}
///@abstract
function BTreeNode() constructor {
name = "BT_TREE_NODE_BASE";
status = BTStates.Running;
visited = false;
children = [];
children_arr_len = 0;
black_board_ref = noone;
static Init = function(){}
static Process = function(){
return BTStates.Success;
}
static ChildAdd = function(_child){
array_push(children, _child);
++children_arr_len;
}
static FindNodeByName = function(_name) {
// Check if this node matches the name
if (name == _name) return self;
// Check children recursively
var _i = 0;
repeat(children_arr_len) {
var _found = children[_i].FindNodeByName(_name);
if (_found != noone) return _found;
++_i;
}
return noone;
}
static NodeProcess = function(_node){
if(_node.visited == false){ // Initial configure
_node.black_board_ref = black_board_ref;
_node.visited = true;
_node.Init();
}
var _status = _node.Process(); // Returning State
if(_status == BTStates.Running and black_board_ref.running_node == noone){
black_board_ref.running_node = _node
}
else if( _status != BTStates.Running and black_board_ref.running_node != noone){
black_board_ref.running_node = noone;
}
return _status
}
static Draw = function(_instance_id) {
// Base Draw method that can be overridden
// Call Draw on all children
var _i = 0;
repeat(children_arr_len) {
children[_i].Draw(_instance_id);
++_i;
}
}
static DrawGUI = function(_gui_x, _gui_y) {
var _color = c_white;
switch(status) {
case BTStates.Running: _color = c_yellow; break;
case BTStates.Success: _color = c_green; break;
case BTStates.Failure: _color = c_red; break;
case BTStates.Off: _color = c_gray; break;
}
draw_set_color(_color);
draw_set_halign(fa_center);
draw_set_valign(fa_bottom);
// Draw status above object
draw_text(_gui_x, _gui_y - 10, name);
if (black_board_ref != noone and black_board_ref.running_node != noone) {
if variable_struct_exists(black_board_ref.running_node, "selector_name") {
draw_text(_gui_x, _gui_y - 25, "Running: " + black_board_ref.running_node.selector_name);
} else if variable_struct_exists(black_board_ref.running_node, "sequence_name") {
draw_text(_gui_x, _gui_y - 25, "Running: " + black_board_ref.running_node.sequence_name);
} else {
draw_text(_gui_x, _gui_y - 25, "Running: " + black_board_ref.running_node.name);
}
}
// Reset draw properties
draw_set_color(c_white);
draw_set_halign(fa_left);
draw_set_valign(fa_top);
}
}
///@abstract
function BTreeComposite() : BTreeNode() constructor{}
///@abstract
function BTreeLeaf() : BTreeNode() constructor{
/// @override
static Draw = function(_instance_id){
// Base leaf Draw method
// This can be overridden by specific task implementations
// For example, GuardianPatrolTask would override this to call DrawWaypoints
}
}
///@abstract
function BTreeDecorator() : BTreeNode() constructor{
/// @overwrite
static ChildAdd = function(child_node){
children[0] = child_node;
children_arr_len = 1;
}
}
/// @param inst_id - Expects an instance id.
function BTreeRoot(inst_id): BTreeNode() constructor{
name = "BT_ROOT";
status = BTStates.Off;
array_push(children, noone);
black_board = {
user : inst_id,
root_reference : other,
running_node: noone,
};
black_board_ref = black_board;
/// @override
static Init = function(){
status = BTStates.Running;
}
/// @override
static Process = function(){
if(black_board.running_node != noone)
NodeProcess(black_board.running_node);
else if(children[0] != noone){
if(status == BTStates.Running)
NodeProcess(children[0]);
}
}
/// @override
/// @param child_node - Expects a BTreeNode.
static ChildAdd = function(child_node){
children[0] = child_node;
children_arr_len = 1;
}
/// @override
static Draw = function(_instance_id){
// Draw children (which will cascade through the tree)
if(children[0] != noone){
children[0].Draw();
}
// If there's a running node with its own Draw method, call it
if(black_board.running_node != noone &&
variable_struct_exists(black_board.running_node, "Draw")){
black_board.running_node.Draw(_instance_id);
}
}
/// @override
static DrawGUI = function(_x = 10, _y = 10, _draw_at_feets = true) {
// Draw root status
var _color = c_white;
switch(status) {
case BTStates.Running: _color = c_yellow; break;
case BTStates.Success: _color = c_green; break;
case BTStates.Failure: _color = c_red; break;
case BTStates.Off: _color = c_gray; break;
}
draw_set_color(_color);
draw_set_halign(fa_left);
draw_set_valign(fa_top);
draw_text(_x, _y, "BT Status: " + name);
var _scaler = _draw_at_feets ? 1 : -1;
var _margin = 20 * _scaler;
// Draw running node if exists
if(black_board.running_node != noone) {
_y += _margin;
draw_text(_x, _y, "Active Node: " + black_board.running_node.name);
_y += _margin;
// If running node has sequence/selector name, show it
var _running_node = black_board.running_node;
if(variable_struct_exists(_running_node, "sequence_name")) {
draw_text(_x, _y, $"Sequence: {_running_node.sequence_name}");
}
else if(variable_struct_exists(_running_node, "selector_name")) {
draw_text(_x, _y, $"Selector: {_running_node.selector_name}");
} else {
draw_text(_x, _y, $"Node!: {black_board_ref.running_node.name}");
}
}
draw_set_color(c_white);
}
}
function BTreeSequence(_sequence_name = "") : BTreeComposite() constructor{
name = "BT_SEQUENCE";
sequence_name = _sequence_name;
/// @override
static Process = function(){
var _i = 0;
repeat(children_arr_len){
if(children[_i].status == BTStates.Running){
switch( NodeProcess(children[_i])){
case BTStates.Running: return BTStates.Running;
case BTStates.Failure: return BTStates.Failure;
}
}
++_i;
}
return BTStates.Success;
}
/// @override
static Draw = function(_instance_id){
// Draw all children in the sequence
var _i = 0;
repeat(children_arr_len){
children[_i].Draw();
++_i;
}
}
}
function BTreeSelector(_selector_name = "") : BTreeComposite() constructor{
name = "BT_SELECTOR";
selector_name = _selector_name
/// @override
static Process = function(){
var _i = 0;
repeat(children_arr_len){
if(children[_i].status == BTStates.Running){
switch(NodeProcess(children[_i])){
case BTStates.Running: return BTStates.Running;
case BTStates.Success: return BTStates.Success;
}
}
++_i;
}
return BTStates.Failure;
}
/// @override
static Draw = function(_instance_id){
// Draw all children in the selector
var _i = 0;
repeat(children_arr_len){
children[_i].Draw();
++_i;
}
}
}
function BTreeInverter() : BTreeDecorator() constructor{
name = "BT_Inverter";
/// @override
static Process = function(){
var _state = NodeProcess(children[0]);
switch(_state){
case BTStates.Failure: return BTStates.Success;
case BTStates.Success: return BTStates.Failure;
default: return _state;
}
}
}
function BTreeSucceeder() : BTreeDecorator() constructor{
name = "BT_Succeeder";
/// @override
static Process = function(){
var _state = NodeProcess(children[0]);
return BTStates.Success;
}
}
/// @description draw_text_spiral_gradual by oliver @pixelchipcode
/// @param text - String to spiral
/// @param x - Center X position
/// @param y - Center Y position
/// @param radius_start - Starting radius
/// @param letter_spacing - Desired space between letters in pixels
/// @param expansion_rate - How quickly the spiral expands
/// DEMO https://bsky.app/profile/pixelchipcode.bsky.social/post/3l7ctn6m2ih23
function draw_text_spiral_gradual(text, center_x, center_y, radius_start, letter_spacing, expansion_rate) {
var len = string_length(text);
var angle = 0;
var radius = radius_start;
// Store original text alignment settings
var old_halign = draw_get_halign();
var old_valign = draw_get_valign();
// Set text alignment to center
draw_set_halign(fa_center);
draw_set_valign(fa_middle);
// Track total angle for expansion calculation
var total_angle = 0;
// Draw each character
for (var i = 1; i <= len; i++) {
var char = string_char_at(text, i);
// Calculate position on spiral
var xx = center_x + lengthdir_x(radius, angle);
var yy = center_y + lengthdir_y(radius, angle);
// Draw the character rotated to follow the spiral
var char_angle = angle + 90; // Add 90 degrees to make characters face outward
draw_text_transformed(xx, yy, char, 1, 1, char_angle);
// Calculate next position
var circumference = 2 * pi * radius;
var angle_change = (360 * letter_spacing) / circumference;
// Update angle and total angle
angle += angle_change;
total_angle += angle_change;
// Gradually increase radius based on total rotation
// Using a logarithmic increase for smoother expansion
radius = radius_start + (expansion_rate * ln(1 + total_angle/360));
}
// Restore original alignment settings
draw_set_halign(old_halign);
draw_set_valign(old_valign);
}
// Example usage in Draw event:
var my_text = "Fools rush in, where wise men never go";
draw_text_spiral_gradual(my_text, room_width/2, room_height/2, 40, 8, 15);
/// @function draw_textbox(x, y, max_width, text, sprite, bg_color, border_color)
/// It uses Scribble's get_box method to calculate the text dimensions, which includes padding,
/// The background and border are now drawn based on the dimensions returned by get_box.
/// @param {Real} _x The x coordinate of the bottom-left corner of the textbox
/// @param {Real} _y The y coordinate of the bottom-left corner of the textbox
/// @param {Real} _max_width The maximum width of the textbox
/// @param {String} _text The text to display in the textbox
/// @param {String} _bg_color The background color in hex format
/// @param {String} _border_color The border color in hex format
function draw_textbox(_x, _y, _max_width, _text, _bg_color, _border_color) {
var _padding = 5;
// Create Scribble text element
var _text_el = scribble(_text, id)
.starting_format("font_tooltip", c_black)
.wrap(_max_width - _padding * 2);
// Get box dimensions
var _text_box = _text_el.get_bbox();
// Calculate total dimensions
var _total_width = _text_box.width + _padding * 2;
var _total_height = _text_box.height + _padding * 2;
// Adjust y coordinate to expand upwards
var _box_y = _y - _total_height;
// Draw background
draw_rectangle_hex(_x, _box_y, _x + _total_width, _y, _bg_color);
// Draw border
draw_rectangle_hex(_x, _box_y, _x + _total_width, _y, _border_color, true);
// Draw text using Scribble
_text_el
.align(fa_left, fa_top)
.draw(_x + _padding, _box_y + _padding);
}
/// @function load_next_sequence(sequence_array)
/// @description Loads the next sequence in the array or executes a function if encountered
/// @param {Array} sequence_array Array of sequences or functions to cycle through
function load_next_sequence(sequence_array) {
// Check if we've gone through all items in the array
if (global.current_sequence_index >= array_length(sequence_array)) {
global.current_sequence_index = 0; // Reset to start if we've reached the end
return;
}
// Get the current item in the sequence array
var current_item = sequence_array[global.current_sequence_index];
// Check if the current item is a sequence or a function
if (is_method(current_item)) {
// If it's a method/function, execute it
current_item();
// Move to the next item in the array
global.current_sequence_index++;
// Recursively call to handle the next item
load_next_sequence(sequence_array);
} else {
// If it's a sequence, layer and play it
layer_sequence_create("Sequences", 0, 0, current_item);
layer_sequence_play(current_item);
// Optional: Add an event listener to automatically move to next sequence
sequence_instance_override_end_position(current_item, true);
sequence_instance_add_listener(current_item,
function() {
// Increment sequence index
global.current_sequence_index++;
// Load the next sequence
load_next_sequence(sequence_array);
}
);
}
}
// Initialize the global sequence index before first use
global.current_sequence_index = 0;
// Example usage in Create event or wherever you want to start sequences
function start_sequence_chain() {
var sequence_chain = [
seq_1, // First sequence
seq_2, // Second sequence
function() { // Function to transition to room
room_goto(room_1);
}
];
load_next_sequence(sequence_chain);
}
/// @description obj_camera Create, added in rm_init
var _viewport_camera_index = 0;
_base_w = camera_get_view_width(view_camera[_viewport_camera_index]);
_base_h = camera_get_view_height(view_camera[_viewport_camera_index]);
var _x_speed = 5;
var _y_speed = 5;
var _border = 128;
camera = camera_create_view(0, 0, _base_w, _base_h, 0, -1, _x_speed, _y_speed, _border, _border);
view_set_camera(_viewport_camera_index, camera);
// follow (we will be following o_player, but once we init Room with player is loaded)
follow = noone;
move_to_x = x;
move_to_y = y;
// zoom_level = 1 Normal view, zoom_level > 1 zoom in, zoom_level < 1 Zoom out
zoom_level = 1;
camera_pan_speed_initial = 0.15;
camera_pan_speed = 1.6;
screen_shake = false;
screen_shake_amount_initial = 2;
screen_shake_amount = screen_shake_amount_initial;
// reset camera to default pan speed CAMERA_RESET_PAN 0
alarm_set(CAMERA_RESET_PAN, 3);
function background_parallax_scrolling(_camera) {
var _layer_id = layer_get_id("Background");
layer_x(_layer_id, lerp(0, camera_get_view_x(_camera), 0.7));
layer_y(_layer_id, lerp(0, camera_get_view_y(_camera), 0.4));
}
///////////////////////////////////////////////////////////////////////////
/// @description camera Step
// 1. set viewport position
x = lerp(x, move_to_x, camera_pan_speed);
y = lerp(y, move_to_y, camera_pan_speed);
var _w = camera_get_view_width(camera);
var _h = camera_get_view_height(camera);
var _center_x = x - _w/2;
var _center_y = y - _h/2;
camera_set_view_pos(camera, _center_x, _center_y);
// 2. follow camera
if (follow != noone and instance_exists(follow)) {
move_to_x = follow.x;
move_to_y = follow.y - global.tile_size/2;
}
#region Apply zoom
// Calculate target dimensions
var _target_w = _base_w / zoom_level;
var _target_h = _base_h / zoom_level;
var _current_w = camera_get_view_width(camera);
var _current_h = camera_get_view_height(camera);
// Smoothly interpolate to the target zoom
var _zoom_speed = 0.1; // Adjust this value to change zoom speed
var _new_w = lerp(_current_w, _target_w, _zoom_speed);
var _new_h = lerp(_current_h, _target_h, _zoom_speed);
// Update camera view size
camera_set_view_size(camera, _new_w, _new_h);
// Adjust camera position to keep it centered on the follow object
if (follow != noone && instance_exists(follow)) {
var _new_x = follow.x - _new_w / 2;
var _new_y = follow.y - _new_h / 2;
camera_set_view_pos(camera, _new_x, _new_y);
} else {
// If no follow object, center on current position
var _new_x = x - _new_w / 2;
var _new_y = y - _new_h / 2;
camera_set_view_pos(camera, _new_x, _new_y);
}
// Recalculate variables for other operations
_w = camera_get_view_width(camera);
_h = camera_get_view_height(camera);
_center_x = camera_get_view_x(camera) + _w / 2;
_center_y = camera_get_view_y(camera) + _h / 2;
#endregion
// limit camera movement within room width & height
var _xx = clamp(camera_get_view_x(camera), 0, room_width - _w);
var _yy = clamp(camera_get_view_y(camera), 0, room_height - _h);
if screen_shake {
// Generate random offset for shake
var _shake_x = choose(-screen_shake_amount, screen_shake_amount);
var _shake_y = choose(-screen_shake_amount, screen_shake_amount);
var _curr_x = camera_get_view_x(camera);
var _curr_y = camera_get_view_y(camera);
var _spd = 0.1;
// Apply offset to camera position
camera_set_view_pos(camera, lerp(_curr_x, _xx, _spd) + _shake_x, lerp(_curr_y, _yy, _spd) + _shake_y);
} else {
camera_set_view_pos(camera, _xx, _yy);
}
// BG scroll effect
background_parallax_scrolling(camera);
////////////////////////////////////////////////////////////////////////////////////////
/// @description Room start
view_camera[0] = camera;
view_enabled = true;
view_visible[0] = true;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment