hpi-swa-lab / godot-pronto

https://hpi-swa-lab.github.io/godot-pronto/
18 stars 4 forks source link

Pronto

Screenshot showing Godot with pronto elements

Pronto is a framework for Godot to make prototyping game mechanics faster. It is not a framework aimed at helping to create entire games faster. The resulting prototypes are a means to quickly explore ideas, throw away the prototype, and only properly implement ideas that turned out well.

Play prototypes created in pronto here.

Function

The main idea of Pronto is to make behavior visible. Our hypothesis is that it will be easier to create and tweak game mechanics. For example, instead of defining numbers in code for the distance a platform moves, we use handles in the game world to visually direct it.

Pronto consists of a set of Godot Nodes called Behavior that can be added to a Godot scene. These behaviors are aspects that, when combined, result in the expression of complex behavior in the Godot scene. All behaviors have visual representation in the scene and primarily function through an event system called Connections. For example, if a timer reaches 5 seconds, a new enemy spawns.

Phase 1: Creating a Prototype

Build a tiny Godot game but focus on a single mechanic. Do not focus on visuals. Polish only where it is essential for the mechanic you are creating. Where possible, use the below advice and helpers instead of code for implementation.

When choosing a mechanic, ask yourself first: Would it be better/faster to build a paper prototype? (If so, consider another mechanic.) What is the minimal feature set I need to implement to see if the mechanic is good? Try and pick a mechanic that is not necessarily the core of the genre but adds an interesting aspect to it and only implement that.

To get started, create a branch with the following pattern:

week-[week number]-[mechanic name]

(e.g., week-0-kart-dash for the tutorial session)

Behaviors

The following list of behaviors act primarily as triggers of events.

Behavior Function
AlwaysBehavior Triggers every frame. Analog to Godot's _process function. Can be paused.
ClockBehavior Triggers after or for a set time. Can be set to paused and to only trigger once. When looking to trigger for a specific duration the Trigger Every Frame property of Until Elapsed needs to be enabled. This will trigger every frame until the timer is elapsed. If you wish to trigger e.g. every second (instead of every frame) you can activate Trigger Every X Seconds and set the Trigger Interval In Seconds to 1. Hint: Interval should not be smaller than typical time between frames (delta).
CollisionBehavior Triggers when something collides with its parent. The collided signal also provides the direction of the collision pointing in the direction from the parent to the node it collided with. NOTE: only works for Area2D, RigidBody2D and CharacterBody2D as of now.
ControlsBehavior Triggers for mouse interactions and offers a convenient set of triggers for keyboard movement. Exact keybindings are chosen via the Player property which allows for up to 3 different controls (Player 1: WASD, Player 2: Arrow keys, Player 3: IJKL). The direction signal can be used for moving a character in combination with a MoveBehavior.
DragBehavior Allows users to drag and drop the parent node using the mouse. Offers signals for hovering/unhovering, dragging/dropping, and moving the node. Supports different types of nodes, including character bodies, rigid bodies, static bodies, sprites, placeholders, and controls.
KeyBehavior Triggers when a single keyboard key is pressed or released.
NodeReadyBehavior Triggers once after the node (and all its siblings) are ready.
SignalBehavior Forwards a signal when triggered, including arguments.

The following list of behaviors primarily cause effects when triggered.

Behavior Function
BindBehavior Optionally reads some properties and then writes one property of its parent. Changes to the properties it reads are synced every frame. The read properties are accessible in the convert expression; the first under value0, the second under value1 and so on. For example, create a Label node, add a Bind node as a child, use text as property and put any expression in its convert field. Another example would use a Clock with a Bind node as a child that uses duration_seconds as a property to change the trigger-time depending on a Value-Behavior.
CodeBehavior It holds an execute() function with both user-definable arguments and a function body, which allows for arbitrary code execution. After execute() is called an after signal is emitted that carries the execution result, which can be used for chaining.
MoveBehavior When triggered, moves its parent. Can be set to move along global or local axes. Supports handling of gravity.
PlatformerControllerBehavior Makes the parent behave like a platformer character, meaning that it can jump, move horizontally, and is affected by gravity. Must be a child of a CharacterBody2D.
QueryBehavior Searches for nodes in the scene and emits signals for results. Properties allow to filter, sort, and limit results. Can be used for tasks such as destroying all enemies in a certain radius, infecting a random player, or finding the nearest health pack.
SceneRootBehavior Provides access to the SceneTree from Godot. It offers the signals node_added(node: Node), node_remove(node: Node) and tree_changed() from the SceneTree. Additionally, it implements three methods for executing lambda functions on all or a specific subset of nodes in a given group.
SignalBehavior Forwards signals when triggered, optionally with arguments. Useful for abstraction/encapsulation of lower-level signals, signal renaming, or logical OR-gating of signals.
SpawnerBehavior When triggered, spawns whatever its child nodes are at its current location in the scene. Single children nodes can be spawned by modifying the optional index argument in each spawning method to just the respective child node. Use -1 as index to spawn all children. Set use_spawn_shape to true to randomly spawn children in the given shape or polygon. When using a Polygon2D do not use it as a child of the spawner.
StopwatchBehavior Starts counting up time when triggered. Can be reset.

The following list of behaviors manage state or communicate visual properties.

Behavior Function
BackgroundBehavior Add to your scene to change the background color of the scene.
CameraShakeBehavior Add as a child of a camera and call its add_trauma function to add shake.
GroupBehavior Draws a shape around its children to group them visually. Only visible in the editor.
HealthBarBehavior Show a rectangular health bar. Offers methods to manipulate the current health and emits a signal when health drops to zero.
InspectBehavior Add as a child of a node to inspect its properties inside the game.
InstanceBehavior Allows you to define a template subtree of Nodes that you want to repeat multiple times without copy-pasting. Add your template as a child of the Instance node, then hover the connection dialog and click the "Instance" button. Note: internally, this creates a "hidden" scene that you need to commit as well. You can thus use "Editable children" in Godot by right-clicking the instance and tweaking properties while inheriting the rest.
PlaceholderBehavior Show a colored rectangle with a label. Useful as a quick means to communicate a game object's function. Functions as a collision shape, so you don't need to add another. Instead of a rectangle a placeholder can also display a sprite instead (use the Sprite Library in the Inspector to choose an existing texture or load your own). Can be flash()ed in a different color.
StateMachineBehavior A state machine to model behavior with different states. Add states by adding StateBehvaior children. Connect other nodes with the trigger method to make triggers available inside the state machine that can be used for state transitions. Special case: The always trigger is called every frame.
StateBehavior Modelling states of a state machine, use as children of StateMachineBehavior. Transition between different states using the on_trigger_received signal. Perform actions in states with the entered, exited and in_state signals.
StoreBehavior Use the Godot meta properties to store state. You can configure it to store values in the global dictionary G and access it via G.at(prop).
ValueBehavior Show a constant you can use in expression visually as a slider. Note that these are shared globally, so create new names if you need to use different values.
VisualLineBehavior Show a colored Line between two Nodes. Useful as a quick visual connection.

Hints

Juice

The following list of nodes don't add new functionality, their purpose is to add a bit of juice to your prototypes. They may not make your prototypes good, but they can add to them.

Juice Function
SparkleJuice Make your scene sparkle!

Connections

Connections are an extension of Godot signals to be more flexible. Connections can be dragged from any behavior to any arbitrary node in a scene. They are the primary means to assemble your game by wiring Behaviors together.

Expressions

Pronto scatters code throughout the scene to be as close to the place where it is relevant. Since there is limited space on the screen, we provide a set of utility functions that make formulating common expressions easier. Some are documented below, you can find all in U.gd. (Feel free to extend these.)

Name Function Example
u(node: Node) Wrap the given Node to expose the utility functions for it. u(other).at("score")
closest_that(cond: Callable) Find the closest node that matches the given criteria. First checks children, then children of parents in a breadth-first search. closest_that(func (n): return n is Node)
group(name: String) group_do(name: String, c: Callable) Get all nodes in the given group. group_do("enemy", func (e): e.queue_free())
at(name: String) Find the "closest" State that has a field called name. If none is found, checks global state in G. at("score")
put(name: String, val: Variant) Find the "closest" State that has a field called name and store the given value. If none is found, checks global state in G. put("score", 0)

Assets

When creating your game/prototype in Pronto, you might want to use some assets to polish the look of your game. Those things are called assets and we already prepared some for you to use.

TileMap

If you want to create a large level you should take a look at the TileMap (default Godot). We already have a TileSet prepared that you can simply load into your TileMap inside the asset folder.

Common Pitfalls

Video Changelog

Phase 2: Extending the Framework

Writing a New Behavior

  1. Choose an icon to use as thumbnail from the Godot source (it is recommended to download the source as ZIP for easily browsing the list).
  2. Create a {Name}.gd file in addons/pronto/behaviors.
  3. As a header for the file, use:
@tool
#thumb("IconName")
extends Behavior
  1. Proceed to write a Godot Node class as regular, e.g. by using @export and signals.

⚠️ make sure to call super implementations of all overwritten methods, e.g. super._ready() and super._process().

When do I have to reload what?

Helpers

As helpers for writing commonly needed functionality, you can use the below:

Configuration
Handles

You can return a list of handles (knobs on the canvas that you can move). See Placeholder and Value behaviors for examples.

Handles can exist in local space of the nodes in the canvas or in the space of the overlay (independent of the canvas' zoom).

Lines and Text Below a Behavior

You can draw lines between nodes or put text underneath your behavior by returning Line instances from the lines() function.

Canvas Override and Events

If you need to add more controls to your behavior, you have to use the canvas override hooks. You can see the implementation of handles in Behavior.gd as an example of this.

Override _forward_canvas_draw_over_viewport(viewport_control: Control) to draw in the overlay space (independent of the canvas' zoom).

Override func _forward_canvas_gui_input(event: InputEvent, undo_redo: EditorUndoRedoManager) to react to events that occur on the canvas. Return true if you want to absorb the passed element.

⚠️ make sure to call the super implementations if you override either of these, or handles will no longer work.

Runtime Values

In ConnectionDebug.gd you can communicate values from the game back to the engine.

Building UI

Hints on Designing new Behaviors

Phase 3: Deploying your Prototype/Game

To deploy your game to hpi-swa-lab.github.io/godot-pronto/ you can use the Build & Deploy Game Action.

This will be done automatically if you created your game via the menu tab Prototype (which automatically creates a branch, folder, main scene and ExportBehavior).

However, if you didn't use this method you can follow this checklist to export your game.

Preparation

Before you can deploy your game check the following:

  1. Make your game the main scene of the godot project.
  2. Make sure that your game and all assets you use are inside your prototype folder (e.g. prototypes/game-myPrototype).
  3. Make sure that your branch-name starts with game- and is named identical to your folder name (e.g. game-myPrototype)
  4. Make sure that no filenames or folders have a space character in them.

ExportBehavior

To automatically export your game do the following:

  1. Add an ExportBehavior (do not rename this node!) anywhere in your game
  2. In the ExportBehavior, you can configure settings such as title, description and authors of the game, as well as how the thumbnail.png gets generated (specified by a delay to automatically take a screenshot)
  3. Set your game scene as the main godot scene
  4. Run your game once so it will generate a thumbnail
  5. make sure to git add the generated thumbnail.png and game_info.json, commit and push!

Once pushed, the system will automatically trigger a CI workflow to deploy your game. If everything went well the system will push your game to the gh-pages branch. This automatically triggers another workflow to rebuild the page. Wait until this workflow has completed before going to the next step. All in all, the game should be live within 3-5 minutes.

Testing

After the workflow executed you can head over to hpi-swa-lab.github.io/godot-pronto/. You should see your game appear in the list of games. Click on the "Play Now" button and check if everything is working.

Deploying Changes

If you have made any changes to your game and want to deploy a new version, you can just repeat this process. Any existing version of your game will be overwritten.

Troubleshooting

If you accidentally trigger the workflow with an incorrect folder name it may execute without generating any errors but will create a new game in the gh-pages branch. This will automatically be rendered into the list of games. In order to delete it, you have to manually remove it from the gh-pages branch. Please be careful when doing so and do not delete any other files other than the projects folder you want to delete.