X Tutup
Skip to content
kurantoB edited this page Jan 25, 2023 · 127 revisions

Click for video

Click for video

Welcome to the Kinematically Reinterpreted Implementation Needing Godot Engine wiki!

Kinematically Reinterpreted Implementation Needing Godot Engine (KRINGE for short) is a 2D platformer boilerplate for Godot that supports ramps and one-way platforms (known as "ledges" in the build).

The bare-bones implementation is just a single scene and a single unit type which is the player, but you can clone the with-npcs branch for the included features. The with-npcs-hitbox branch lacks documentation, but it has all the NPC features of with-npcs as well as the building blocks for combat.

The boilerplate leaves room for you to build custom stages with your own assets, as well as customizing different unit types along with their associated behaviors and sprites.

Prerequisites

Before using this boilerplate, you would need to be already somewhat familiar with Godot basics, especially TileMap, GDScript, scene inheritance, and sprites/animations.

Boilerplate Features

If you run the project straight out of the box, you can control the player through left/right arrow keys, while being able to jump with Z. You can pause with the Enter key. If you fall off the stage, you just fall indefinitely.

Understanding the Assets

res://Scenes/SandboxScene.tscn contains a TileMap Stage where the building blocks of a level are assembled. The tile set used here is res://Tile Sets/TestTileSet.tres.

SandboxScene's Player node is an instance of res://Units/Player.tscn. Player has a script that inherits from res://Scripts/Unit.gd. The one exported field for all units is unit_type, which in the player's case is set to 0 for PLAYER.

The base node in SandboxScene.tscn has a script res://Scripts/GameScene.gd and takes a string parameter "Tile Set Name". Here, it is set to "TestTileSet" - this can a tile set of your choice, provided it is configured under the same name in res://Scripts/Constants.gd.

Mapping Stage Element Types to TileSet Elements

Constants.gd contains a dictionary TILE_SET_MAP_ELEMS that maps each tile set name a to sub-dictionary that maps MapElemType to a list of tile indices (the value type returned by TileMap's get_cellv()) of the tile set you're using. This dictionary is used to determine which stage element each specific tile in your tile map corresponds to. Here's a rundown of the different elements:

  • SQUARE - collision box that takes up the full tile
  • SLOPE_LEFT - 1x1 ramp that spans one tile corner to corner, left-right incline
  • SLOPE_RIGHT - 1x1 ramp that spans one tile corner to corner, right-left incline
  • SMALL_SLOPE_LEFT_1 - lower portion of a 2x1 ramp that spans 2 tiles side by side, left-right incline
  • SMALL_SLOPE_LEFT_2 - upper portion of a 2x1 ramp that spans 2 tiles side by side, left-right incline
  • SMALL_SLOPE_RIGHT_1 - lower portion of a 2x1 ramp that spans 2 tiles side by side, right-left incline
  • SMALL_SLOPE_RIGHT_2 - upper portion of a 2x1 ramp that spans 2 tiles side by side, right-left incline
  • LEDGE - one-way platform, its only collidable surface is the top edge of the tile

Tweaking Gameplay Parameters

Towards the end of Constants.gd, you'll see several constants that affect gameplay mechanics.

  • UNIT_TYPE_MOVE_SPEEDS, UNIT_TYPE_JUMP_SPEEDS, GRAVITY, and MAX_FALL_SPEED are pretty self-explanatory
  • SCALE_FACTOR refers just to the stage elements and player sprites now, but this constant comes in handy if there's anything else you want to scale visually
  • GRID_SIZE is the length of one map unit in pixels, i.e. the length/width of your stage tiles (pre-scaling)
  • ACCELERATION refers to how quickly units pick up speed or slow down as they move
  • QUANTUM_DIST is not really specific to game mechanics and you can choose to ignore it. Basically, there's always a small distance between a player's collision checking points (its hitbox, if you will) and any given collidable surface in the stage. For example, there shouldn't be a situation where the player's feet occupy the same point in world space as the floor they are touching, to avoid complicating things. QUANTUM_DIST needs to be small enough to not be noticeable during gameplay.

Constants.gd also has a dictionary CURRENT_ACTION_TIMERS which is explained further in the Defining a Custom Unit section, but it binds a duration to a UnitCurrentAction. Here in the boilerplate, you can control how high you can jump by defining how long your jump input should be registered.

Making a Custom Level

  • If you're using a custom tile set, save it in res://Tile Sets/.
    • In res://Scripts/Constants.gd, make a new entry in the TILE_SET_MAP_ELEMS dictionary for your custom tile set
  • Make a new scene and attach res://Scripts/GameScene.gd. Set the "Tile Set Name" script variable to the corresponding tile set name in TILE_SET_MAP_ELEMS in Constants.gd.
  • Make a TileMap child node to your root node. It must be named Stage. Build your tile map using the tile set corresponding to the TILE_SET_MAP_ELEMS entry referenced by "Tile Set Name". Note: only put stage elements into this tile map, no background elements, etc.
  • Add a player instance under your root node. It must be named Player. (The boilerplate resource is res://Units/Player.tscn, which you are free to edit and customize.)
  • Add a Camera2D object as a child of the player instance, which will be the main camera. It must be named Camera2D. The viewport will shift when the player changes their facing direction. You can configure its smoothing in the editor.

Defining a Custom Unit

A unit follows an archetype, which comes with its own archetype-specific properties, behaviors, and sprites.

  • res://Units/ exists to house unit archetypes. In your custom unit scene root node (which should be a Node2D), attach your custom unit script from the res://Scripts/Units/ directory. Like Player.gd, it should inherit from res://Scripts/Unit.gd. (If you're editing res://Units/Player.tscn itself, then you'd want to be editing res://Scripts/Units/Player.gd.)
    • Unit.gd consumes an int parameter "Unit Type", which for Player would be set to 0. If you're making a non-player unit, you'd need to define the unit type in Constants.gd and set this parameter to the int representing its UnitType enum value in Constants.gd.
  • The root node's children are the Sprites and AnimatedSprites that belong to this unit type. The next section includes a description of how to bind them to your custom unit type.

UnitType in Constants.gd

Making a new UnitType, or further customizing an existing one, comes with its own set of housework. See below for the relevant Constants.gd sections that allow you to configure a custom unit type.

  • enum UnitType - A list of different unit types. You'll need to add a new enum here.
  • enum ActionType - Add new actions as you see fit (for example, crouch, recoil, etc.). The pool of actions available for a given unit type is maintained in UNIT_TYPE_ACTIONS.
  • enum UnitCondition - Every unit contains a set of conditions, which are defined here. Treat each condition as an individual axis belonging to the player, within which they can only assume one position at any given time. Their default values are configured in UNIT_TYPE_CONDITIONS. Configure the default conditions for your unit types there.
    • A unit can only have one CURRENT_ACTION at any given point in time. The pool of current actions available for a given unit type is configured in UNIT_TYPE_CURRENT_ACTIONS. A CURRENT_ACTION of type enum UnitCurrentAction is something that persists across multiple frames and shouldn't be confused with actions under ActionType, which are only evaluated for a specific frame.
    • Likewise, units can only have one MOVING_STATUS at any given point in time. enum UnitMovingStatus is the pool of available moving statuses. By default, you only have IDLE and MOVING, but you can add more statuses, like dashing, if you want that feature.
  • CURRENT_ACTION_TIMERS is to configure durations for select UnitCurrentActions (that have a timeout) per unit type. For now, it's only used to configure how long a player's jump command is recognized before gravity takes over and the player's CURRENT_ACTION automatically becomes UnitCurrentAction.IDLE.
  • ENV_COLLIDERS is where the (environmental) collision detection for a given unit type is configured. To take PLAYER's example, we have 6 collision detection points - one at the player's origin (0, 0), or where their feet are located, one at the top of their head (0, 1.5), and a few in between. The unit of measure is in grid units. (More about grid space later.) The list of Directions describe which directions the given collision detection point checks for. For example, the one at the player's feet checks for left, down, and right collisions.
  • UNIT_SPRITES groups a given unit type's sprites into different classes. The sprite configuration is mostly intended to be parsed by the custom unit script unless they have to do with being idle, moving, or jumping, which are assumed universal across all unit types and hence parsed by res://Scripts/Unit.gd. The "Nodes" portion of the config are extracted among the children of the unit root node (i.e. the Sprites and AnimatedSprites).

How do all these Unit-Specific Customizations Come Together?

Let's take a look at how one simple action is propagated through the game logic, to give an idea of what customizing new/existing actions and new/existing unit types would entail. We can trace through the scenario where a player starts off at a state of being idle, to having received the command to jump.

  1. In the initial idle state, the unit's conditions are as follows:
    • CURRENT_ACTION = IDLE
    • IS_ON_GROUND = true
    • MOVING_STATUS = IDLE (This one condition isn't really relevant to our scenario, so we'll ignore it going forward.)
  2. In this frame, the player's input for jumping (Z key) is to be registered.
  3. _process(), Godot's default per-frame handler, is called from res://Scripts/GameScene.gd. The following steps break down the components of this call.
  4. reset_actions() is called on the player unit to provide a blank slate in which this frame's player actions will be registered. Next, the base method handle_input() does just that via handle_player_input() in GameScene.gd. By default, input is interpreted within the GBA control scheme, so you see constants like Constants.PlayerInput.GBA_A, which is mapped to the Z key.
    • The actions dictionary in Unit.gd is updated to have its JUMP action type mapped to true. In other words, we're setting the "jump" flag for this unit for this frame.
  5. process_unit() in Unit.gd is called. Think of this function as the unit "declaring its intention." In other words, the movement speed is set according to the actions the unit is taking this frame, without taking the environment (i.e. the stage) into account just yet. The execute_actions() function of both Unit.gd and Player.gd is called. Here is where we check each action type available for this unit type to see if the unit is taking the action this frame. Since we're jumping this frame, we have a function that handles it: the jump() function in Unit.gd. This is a special case in that jumping is assumed to be universal across all unit types - any action that is specific to a particular unit type should be handled in said unit type's respective class.
    • CURRENT_ACTION is set to JUMPING
    • We update the unit's v_speed (vertical speed).
    • IS_ON_GROUND is set to false
    • We set the unit's sprite to the Sprite node indicated by 0th element of the JUMP node list, as defined in UNIT_SPRITES in Constants.gd. In this case, it's the Jump1 node in the Player scene.
  6. interact() in StageEnvironment.gd is called on the unit. Here, we take the unit, which already "declared its intention" via its updated movement speed, and apply gravity and collision calculations to it. The result is a new movement speed (which may or may not end up being the same.) During the first frame of jumping, there isn't much in the way of collisions, so all we're doing is applying 1 frame's worth of gravity to the unit. The slight decrease in the unit's v_speed won't accumulate over the next few frames if the player keeps the jump key pressed down because the next process_unit() calls would set v_speed to whatever the jump value is again.
  7. The unit react()s and executes on the movement speed (i.e. the v_speed (vertical speed) and h_speed (horizontal speed)) it ends up with at this point. We arrive at its new pos vector (which is distinguished from position in that pos is in grid space instead of world space). 1 unit of grid space corresponds to the number of pixels defined by GRID_SIZE in Constants.gd. Grid space shares the same origin with world space, although contrary to world space, the y value increases from bottom to top. All measurements and calculations in KRINGE are done in grid space.
  8. Here are the unit's resulting conditions.
    • CURRENT_ACTION = JUMPING
    • IS_ON_GROUND = false

Exercises

Try the following exercises!

Additional Notes

Camera jitter may be the cause of horizontal lines appearing on tile maps. To prevent this, go to Project Settings > Rendering > 2d and turn on Use Gpu Pixel Snap.

If you find any bugs or have any comments, I'm here for it! My contact info is below.

Usage permissions: This is free to use, but I would kindly request credit given where it's due.

X Tutup