Sheet management

An article on function sheets, action sheets, value sheets, top level sheets... oh, and event sheets of course. And emojis.

What is an event sheet? A sheet that contains events, right? But what if there is a sheet that only has global variables or constants? Or one containing functions exclusively? Or custom actions, for that matter.

In the last few years I've been using a system that involves having top level event sheets and four subfolders, one for sheets with custom actions, one for sheets with functions, one for sheets with global values (variables and constants) and one for actual event sheets. The subfolders are named Actions, Events, Functions and Values, all sheets in the subfolders are prefixed with the letters "a", "e", "f" and "v" respectively, so it's easier to navigate the open tabs.

In this article I go through the details of this setup (with some sidenotes here and there). I have a blank project template that you can download with the folders and most commonly used event sheets (most of them are empty, prefixed with an underscore).

A slightly weird thing I also like to do is to add emojis at the end of event sheet names. They act a bit like icons, so when I have a lot of tabs open, it's easier for me to find the one I'm looking for. Emojis also signify the fact that the tab is an event sheet, and not a layout.

Sidenote
If you put emojis in the names of Construct 3 event sheets (or other primitives, eg. objects) and save your project as a folder instead of a c3p file, make sure that your file system and apps that handle your project folders are okay with emojis in the file names. For example, Dropbox isn'tΒ  - at the time of writing, it refuses to sync files with emojis in their names. Using c3p project files inside a Dropbox folder is a workaround for this issue, since those are really zip files, which handle emoji filenames in them without problems.

Okay, let's dive in!

Top level sheets

My rule is to only have sheets on the top level if they are assigned to at least one layout. When I'm prototyping, I usually start dumping everything in a top level event sheet, then organize it later by moving everything to other sheets. I like to go fast and do periodic clean-ups after things are in a working state.

For simpler layouts (eg. the crappy low effort menus that I usually make for jam games and prototypes), I just put all events in a top level sheet, since there aren't too many anyway.

For complex ones (like levels) I create a top level event sheet (eg. Level πŸ—ΊοΈ) and only put event sheet includes in it, nothing else. There's no need to include sheets that only contain functions, global variables, constants or custom actions, as those are all globally available by default. Actual event sheets do have to be included in top level event sheets in order to run.

Actions folder

This folder is for sheets with custom actions only, one sheet per object type (or family). The sheet's names match the object or family name, prefixed with the letter "a" (short for "action") and suffixed with an emoji, eg. aPlayer πŸ§‘, aEnemy πŸ‘Ύ, etc.

Common custom actions include spawn (which spawns an instance of its object type or family) and tick (which is called in an every tick event and does various things with the instance). If a game has players and/or enemies with health, I usually have one called changeHealth on the object type which is called when an instance is damaged or healed. This action takes care of changing the health instance variable, doing audio and visual feedback based on the change, checking if health is zero and doing the destruction of the instance, if needed.

Events folder

This folder is for actual event sheets, only containing events (sometimes enclosed in groups). These sheets have to be included in at least one top level event sheet in order to work (unlike function, action and value sheets). I never put any global variables, constants, custom actions or functions in these sheets.

Sheets in this folder are named according to the kind of conditions (mostly triggers) that are in them. Here are the ones I use most frequently:

  • eCollisions πŸ’₯ for "On collision with another object" events only. I used to put overlap checks in here too, but nowadays I usually do those inside tick custom actions instead (and other non-trigger events too). I find it very handy to have all collision checks in a single sheet.

Sidenote
When setting up collision checks, I try to set the first object to the one that (probably) has fewer instances in a layout, and the second to the one that (probably) has more. I read somewhere that this has better performance. Also don't forget to set a reasonable collision cell size for your layout, the default is usually way too big.

  • eCreated ✨ for "On created" events only, and only for object types / families that have instances on a layout at start, not ones that are dynamically spawned during runtime. In the former case, I usually make an init custom action which initializes the object and call it from the "On created" event.
    If an object type only spawns dynamically during runtime, I usually have a spawn custom action for it, which takes care of the spawning and any initialization as well. I keep at least one instance of dynamically spawned objects is in a "repository layout" - nowadays I name it @ Templates. Most official examples have one called ObjectRespository.
  • eTimers ⏰ for "On timer" events only.

Sidenote
Don't forget to add a "for each" to "On timer" events if there's a chance that multiple instances of the same object might finish their timer in the same tick. If that happens and there's no "for each", only one instance will be picked by the "On timer" trigger, which is a bit of a footgun, if you ask me (by the way I'm also planning an article on Construct 3 footguns). That said, there's a warning in the official docs about this behaviour.

  • eTweens πŸŒ“ for the Tween behaviour's "On finished" events only. As far as I know, there's no need to "for each" those like Timers.
  • eInput_Global πŸ”˜ andΒ eInput_Level πŸ•ΉοΈ for anything that is input related: keyboard, mouse, touch and game pad events including triggers and non-triggers (eg. "is down"). eInput_Global πŸ”˜ contains input events which have to run on multiple or all layouts (eg. toggling full screen or restarting the whole game, etc). This also means that it's included in multiple top level event sheets. eInput_Level πŸ•ΉοΈ only has input handling related to the level and I usually have events split into multiple groups: one for player movement (or one for each player in a local multiplayer game), one for UI, one for developer cheats and so on.
  • eSystem ⏱️ exclusively for events of the "System" category. Usually it contains a single "On start of layout" event and a single "Every tick" event with multiple function and custom action calls. Any other triggers from the "System" category also go in this sheet, eg. "On signal", "On canvas snapshot, "On save complete", etc.

Here are some less frequently used ones:

  • eAJAX πŸ“‘ for trigger events of the AJAX object, like "On completed" and "On error".
  • eAnimations 🎞️ for animation related trigger events, like "On finished" and "On frame changed".
  • eMovement πŸƒ for movement behaviour related triggers, like "On landed" for the Platform behaviour, "On step" for the Bullet behaviour, "On target acquired" for the Turret behaviour, et cetera.

Functions folder

This is for sheets containing functions only.

There's usually one called fUtilities 🧰, which contains a handful of frequently used math functions which calculate and return values like volumePercentToDb (for converting a 0-100 percent value to decibels for Audio object actions), distance3D (like the system expression distance but for 3D coordinates), expDecay (the function I usually use instead of lerp).
I frequently have some helper functions like toggleFullscreen which requests or cancels full screen mode and toggleLayer which simultaneously toggles the visibility and interactivity of a given layer.

In almost all projects I have a sheet called fLevel 🎬, which contains "game life cycle" functions like initLevel, endGame and other game-specific functions that do a lot of things with a lot of objects.

In some projects I used to have a sheet called fAudio 🎡 for functions like playSFX and playBGM to play sound and music according to the current volume settings, with optional debouncing and random variations for sound effects. Nowadays I create an aAudio 🎡 sheet in the Actions folder and put similarly named custom actions on the Audio object.

Sidenote
My blank project template also includes the fRules πŸ“œ sheet, with a very specific set of functions, for loading a default rule set for the game from a JSON file and allowing URL parameter based rule overrides to help with balancing / testing. It's a whole thing, I'll probably write an article about it at one point.

Values folder

The Values folder is for sheets containing only global variables or constants. I have a fixed set of 4 sheets in this folder:

  1. vPrimitives 🍞 containing global constants for a handful of "generic" values and characters that I use frequently in expressions:
    • NOTHING with the numeric value of -1, which is the next best thing to null (which is not a thing in Construct). Eg. if I have an instance variable for storing object UIDs, I use this to "clear" its value (or to check if it's set or not), since -1 not a valid UID. Some system expressions also return -1 (eg. find when there are no results), so it comes handy for those too.
    • NO and YES: with the numeric values 0 and 1 respectively. I almost never use booleans in my projects, instead I use numbers with these values. My main beef with booleans in Construct 3 is that I can't pass a dynamic value to a function or custom action with a boolean parameter. When calling one, I have to use a checkbox to either pass true or false, I cannot pass the result of an expression or a variable. Because of this I refrain from using boolean variables in functions and custom actions, and use the numbers 0 and 1 instead.
    • EMPTY, which is an empty string, the NOTHING of string variables. I use it to clear a string variable or to check if it's empty.
    • COMMA, UNDERSCORE, SPACEΒ and DOT: single character string constants that contain a comma, an underscore, a space and a dot (or full stop, period, etc) respectively. I use DOT when writing JSON object path expressions, the rest are for dealing with comma / underscore / space delimited strings (aka. "cheapskate arrays"), eg. with tokenat and tokencount.
  2. vNames πŸ—ƒοΈ contains global constants that act as "enums" or "macros" (not sure of the right terminology). I have a whole article on "global names" that goes into details on what is in this sheet.
  3. vConfig πŸ’Ž contains global constants prefixed with C_, which are all magic numbers or magic strings that do not need to change during runtime. The contents of this sheet are specific to the project, but here are some real-life examples from our game Wordpile:
    • C_Tile_TweenOut_Time: tween time for fading out a Tile object.
    • C_DIR_Layouts: path of the folder that contains level layouts (which are loaded via AJAX).
    • C_UI_Hold_Interval: the amount of time a player needs to hold a button to activate its secondary function.
    • C_Color_Red: a certain red color value (in this case -281479422673919) that I use in a some "set color" actions.
    • C_Player_Count_Max: hard limit of players that can join a local multiplayer match (the game supports 1-4 players).
  4. vState ♻️ only contains global variables (and no constants) that hold internal state of the game. These are camel cased by default, but sometimes I group them with prefixes and use an underscore after a prefix. Some random examples:
    • locale: contains 2 character code for the currently selected UI language (eg. "en"), used for configuring the Internationalization plugin after the a language is chosen on the UI.
    • volumePercentBGM and volumePercentSFX: 0-100 values of the background music and sound effects.
    • level_seed: current seed of the level used for initializing the AdvancedRandom object.
    • level_currentPlayerId: contains which player's turn is it in a multiplayer game (0 for first player, 1 for second, etc).
    • level_penaltyTotal: when the level ends, the player gets an amount of penalty points based on how they did and it gets tallied up in this variable.

That's about it, really. The system hasn't changed much recently, but sometimes I do decide to rename or restructure a few things. I'll try to keep this article updated if that happens.

This article was updated on