Skip to content

rimuy/GameJoy

Repository files navigation

GameJoy

A simple class-based input library made with roblox-ts.


CI Status License MIT Package

Installation

npm

Simply execute the command below to install it to your roblox-ts project.

npm i @rbxts/gamejoy

Wally

For wally users, the package can be installed by adding the following line into their wally.toml.

[dependencies]
GameJoy = "rimuy/[email protected]"

After that, just run wally install.

From model file

Model files are uploaded to every release as .rbxmx files. You can download the file from the Releases page and load it into your project however you see fit.

Features

  • Class-based

    Because action bindings are actually classes, the user has the ability to check whether an action is active or not, and to use its respective methods and events for better manipulation. We want to choose what kind of action we want to use by destructuring Actions.

    These are all the action classes that are currently available:

    import { Actions } from "@rbxts/gamejoy";
    
    const { Action, Axis, Dynamic, Manual, Middleware, Optional, Sequence, Synchronous, Union, Unique } = Actions;
    • Aliases

      To shorten some existing keycode names, the library provides a ton of aliases that can be used instead. All aliases can be found here.
  • Context oriented

    In order for actions to trigger, they must have a context where they belong to. A context contains all the actions that bound to it, and is responsible for making them successfully trigger.

    import { Context } from "@rbxts/gamejoy";
    
    const context = new Context({
            /**
             * Limits the amount of actions that can trigger if those have any raw action in common.
             * If set to 0, this property will be ignored. (defaults to 0)
             */
            ActionGhosting: 1,
            /**
             * Applies a check on every completed action. 
             * If the check fails, the action won't be triggered. (defaults to () => true)
             */
            OnBefore: () => !!Player.Character,
            /**
             * Specifies that the action should trigger if gameProcessedEvent matches the setting.
             * If nothing is passed, the action will trigger independently. (defaults to nil)
             */
            Process: false,
            /**
             * Specifies if the actions are going to run synchronously or not.
             * This will ignore the action queue and resolve the action instantly. (defaults to false)
             */
            RunSynchronously: false,
    });
    
    context.Bind(["MouseButton1", "ButtonX"], () => {
            CharacterController.Attack();
    });
    • Queued actions

      GameJoy contains a built-in action queue that automatically removes a resolved action from the queue and then executes the next one that was triggered, if the same is still pending. Every action that is successfully triggered, is sent to that queue, which will have the following behavior:

      Situation 1: You press Q, then E. Since Q was the first one to be triggered, it's gonna be instantly executed. Q lasts 3 seconds and E is supposed to be triggered after it ends. If the E key becomes up, it's gonna be removed from the queue.

      Situation 2: You press Q, E and then R. This follows the same pattern from situation 1. When you cancel E, R is gonna be the next action to be executed, since its position in the queue was right after E.

      In both cases, everytime an action is added to the queue, it will try to execute whatever action is in its first position. Once the action resolved or rejected, the process will start all over again, until the queue becomes empty.

      This behavior can be disabled by setting the context's RunSynchronously option to true.

    • Event bindings

      Actions are not the only way to register something into the context, it's possible to even use events there! Events requires identifiers, so that it can be possible to unbind them when needed.

      context.BindEvent("onCharacterDamaged", CharacterController.Damaged, (oldHealth, health) => {
              const damage = oldHealth - health;
              print(`You lost ${damage}HP!`);
      
              task.wait(0.3); // The player must wait 0.3 seconds before being able to counter-attack.
      });

      If you want an event connection that doesn't use the queue, but still want it to pass the context's OnBefore check, and to be disconnected when using the context's unbinding methods, you should use Context.BindSyncEvent.

      context.BindSyncEvent("onRender", RunService.RenderStepped, (delta) => {
              print(delta);
      });
  • Utilitaries

    There are some utilitary functions available, such as typechecks. Those are located in the Util namespace.

Creating an Action

An action is an object that holds information about inputs that can be performed by the player while in a context. This can vary from a single action, to multiple ones. Actions be nested! which means that actions that accept multiple entries can have actions that contain other actions, and so on.

const action = new Action("Q");

context
        .Bind(action, () => {
                print("Q was pressed!");
        })
        .BindEvent("onReleased", action.Released, () => {
                print("Q was released!");
        });

Action also accepts an object as the second parameter, used for configuration. The amount of times a key needs to be pressed and the maximum time between each press can be set up.

let isRunning = false;

const runAction = new Action("W", {
        Repeat: 2,
        Timing: 0.3,
});

context
        .Bind(runAction, () => {
                isRunning = true;
        })
        .BindEvent("onRunningStopped", runAction.Released, () => {
                isRunning = false;
        });

Everytime an action is triggered, it'll fire the Triggered event.

Raw Actions

An action entry doesn't necessarily need to be an instantiated class, it could be a string, number or an enum item corresponding to the correct name or value from Enum.KeyCode and Enum.UserInputType.

context.Bind("F", () => {
        print("F was pressed!");
});

context.Bind(["Q", "E"], () => {
        print("Q or E was pressed!");
});

Of course, you won't be able to use any event that you could use with an action object.

Filtering multiple inputs

Sometimes don't you want two or more inputs to trigger the same action? Well, if so, Union is what you want! It accepts an array of action-like entries as a parameter.

In this example, you create an union of F and ButtonB. If one of these keys are pressed, the action will be triggered.

context.Bind(new Union(["F", "ButtonB"]), () => {
        print("You pressed either F or ButtonB!");
});

In addition, an union can also be a raw action instead of an action object! All you need to do is to replace its Union class with its own entry array instead.

context.Bind(["F", "ButtonB"], () => {
        print("Easier to write :D");
});

A single raw action gets transformed as Action, whereas an array of those gets transformed as Union.

Triggering an action at once in a collection

With Unique, it's possible to have an Union that won't trigger if a child action is already active. Making it unique!

context.Bind(new Unique(["C", "V"]), () => {
        print("Either C or V... but one must be inactive for the another one to work.");
});

Composing inputs into an action

What about having to press multiple keys at the same time to trigger an action? Well, Composite is what you're looking for. In a composite, the action will only trigger if all of its children actions are completed.

context.Bind(new Composite(["J", "K", "L"]), () => {
        print("You pressed J, K and L!");
});

Using a specific order of inputs

Now if you want a composite, but need it to require the actions to be executed in a specific order, try Sequence!

context.Bind(new Sequence(["LeftAlt", "E"]), () => {
        print("Yay");
});

Sequence is cancellable. When one of the keys is released, it'll trigger the Cancelled event. If there is already an action being executed and the composite was already queued, it'll remove the composite from the queue, preventing it from being triggered. This doesn't apply if RunSynchronously is set to true.

context.BindEvent("onCancel", sequence.Cancelled, () => {
        print("Composite was cancelled.");
});

Making one of the inputs optional

Some variants that requires multiple entries (Composite, Sequence and Unique), can contain an optional action.

context
        .Bind(new Composite(["F", new Optional("G")]), () => {
                // ...
        })
        .Bind(new Sequence(["F", new Optional("G")]), () => {
                // ...
        })
        .Bind(new Unique(["F", new Optional("G"), "H"]), () => {
                // ...
        });

In both Composite and Sequence, the action is gonna be triggered if F is pressed, and then triggered again if G is pressed while F is still being hold. The difference is that in Sequence, the optional action must be activated in the right order.

In Unique, the optional action will be able to break its parent object rules, triggering Unique even if a child action is already active.

It's common to store the optional action in a variable to get its information later, like knowing whether it's pressed or not.

const optional = new Optional("G");

context.Bind(new Composite(["F", optional]), () => {
        if (optional.IsActive) {
                print("Do a barrel roll!");
        }
});

Updating an existing action

Thanks to Dynamic, updating actions is a very easy task. You can store any action-like inside it to make it updatable. Since dynamic actions are limited to a type, you'll need to create a type to filter what input will be available for the action to update.

type ActionThatChanges = "X" | "Y" | "Z";

const dynamic = new Dynamic<ActionThatChanges>("X");

context.Bind(dynamic, () => {
        print(dynamic.RawAction);
});

task.wait(1);

dynamic.Update("Y");

task.wait(1);

dynamic.Update("Z");

For multiple input actions, you'll want to include the type of all its entries into your type.

type ActionThatChanges = "X" | "Y" | "Z" | "A" | "B";

const dynamic = new Dynamic<ActionThatChanges>(new Composite(["A", "B"]));

The AnyAction type can be used if you don't want to filter the entries.

Using an Axis

Axis provides support for inputs that have a continuous range. The action is triggered everytime the input is changed. This is mostly used with joysticks, for when you want to map player movement using an analogic button, or to know how pressed down are its upper buttons.

const gamepad1 = new Axis("Gamepad1");
const mouse = new Axis("MouseMovement");
const thumbstick = new Axis("Thumbstick1");
const l2 = new Axis("ButtonL2");

context
        .Bind(gamepad1, () => {
                // Last controller button that was changed
                print(gamepad1.KeyCode);
                print(gamepad1.Delta);
                print(gamepad1.Position);
        })
        .Bind(mouse, () => {
                print(mouse.Delta);
                print(mouse.Position.X, mouse.Position.Y);
        })
        .Bind(thumbstick, () => {
                print(thumbstick.Position.X, thumbstick.Position.Y);
        })
        .Bind(l2, () => {
                print(l2.Position.Z);
        });

Creating a conditional action

Sometimes you want to specify when an action can be triggered, but don't want to configure the context to do so, because that would apply the check for all the bound actions. Middleware accepts a callback that can be used to set a condition to your action.

const timeMiddleware = () => os.time() % 2 === 0;

context.Bind(new Middleware("M", timeMiddleware), () => {
        print("Works half of the time, haha...");
});

Whitelisting actions from async behavior

Just like the above, Sync also aims to apply a configuration trait to a specific action, this time replicating the RunSynchronously option, making a specific action able to trigger synchronously.

context
        .Bind("A", () => { task.wait(5); })
        .Bind("B", () => print("Needs to wait till A is done..."))
        .Bind(new Sync("C"), () => {
                print("Will be executed even if there is already a pending action!");
        });

Manually triggering an action

Manual is an action-like alternative to a bound event. Contains a Trigger method with custom parameters that can be used in its listener.

const manual = new Manual<[string, number]>();

context.Bind(manual, (name, age) => print(name, age));

manual.Trigger("Kevin", 19);

Removing actions

To remove any action that is bound to a context, simply use the Unbind method. All bound actions can be removed at once when using UnbindAll.

const action1 = new Action("X");
const action2 = new Action("Y");
const action3 = new Action("Z");

context
        .Bind(action1, /** ... */)
        .Bind(action2, /** ... */)
        .Bind(action3, /** ... */)
        .Unbind(action2) // Unbinds action2 from the context
        .UnbindAllActions();    // Unbinds all of the remaining bound actions

License

This project is MIT licensed.