Hi, been a while since my last blog post.
So lately I’ve been thinking about switching engines for my game. I must admit that in a world where Unity is getting more and more tools and has a ton of high quality addons, community made assets, tools and tutorials made by professionals, where Unreal is getting consistent quality updates and where literally every single engine looks amazing, switching engines is very tempting.
That’s not what I seek in an engine however. I don’t care how many people use it or how many tools there are. What I care about is how easy it will be for me to make my game. For that Construct really is on top of a lot of competition. Crushing, in my opinion, Game Maker, Godot and Multimedia Fusion. Also, Unity and Unreal aren’t very suited for 2D development, so using them for a 2D game might prove counterproductive.
However, one thing Unity has that Construct doesn’t is classes.
Classes?
For those of you who might not know what a class is, it’s basically a way in Object Oriented Programming to define a type of object and assign it values and functions.
For instance, if I create an Enemy class, I might give it an HP variable and give it a method that will decrease its HP each time the player hits the enemy.
Yes. Kind of.
How construct works is that it lets you add plugins in your project and work with that plugin’s instances: Yes, I could make a Enemy Sprite object, give it an HP instance var and create a damage function that will reduce that enemy’s HP, and the result would virtually be the same.
In fact, plugins are classes and its ACEs are its methods and properties.
You can’t create new classes from these plugins. Sure, you can create new plugins, but you’re not gonna create an “Enemy addon” that will just be a copy of the Sprite plugin with additional methods.
Sure, you can create behaviors that will add methods and eventually make your sprite work like an enemy.
That’s not the point. Creating classes within Construct’s event sheet would allow us to do many things, but one in particular.
Class inheritance
A good practice in programming is “DRY” Don’t Repeat Yourself. That means that if you have 2 objects that both have HP and code that reduce said HP, then you should be able to write that code only once across both objects.
That’s where class inheritance comes in handy. If I know both my player and my enemy are going to have HP, then I can create a “Health” class, then create my “Player” and “Enemy” classes and have both inherit from “Health”. That way my HP code is in one place and is common to every object.
We don’t have that in Construct sadly.
This is where my reasoning starts:
Why don’t we have class inheritance in Construct?
Can we have something that works like classes?
Actually, why only classes? Can’t we define a set of optimal eventing practices that will allow us to write good events in Construct?
Optimal? What’s optimal?
Right. In order to reinvent the wheel, we should probably start by looking at what a wheel is.
A code can be considered good if we can give it good rates when measured with four generalised attributes:
- Maintainability: How easily the code can be maintained (aka, changed, expanded upon or fixed)
- Dependability: How reliable the code is (aka how much it can be trusted with doing what its supposed to do, and not produce bugs)
- Efficiency: How efficiently it does what it’s asked to do, by using as little resources as possible.
- Usability: How easy the code is to use and how many things it can be used for (aka how versatile the code is)
Now that we know what to aim for in general, let me add a few additional attributes the events should have in order to be adapted to Construct.
- The events should use a little 3rd party plugins as possible, unless it is necessary, and the plugins are reliable.
In fact, broken or deprecated plugins are the best way to lock you in a tough position when they decide to stop working, or to not be compatible with another plugin. That’s even worse when your whole project revolves around that one plugin, or when you need to upgrade from C2 to C3 and that plugin isn’t supported anymore.
- The events should use as many vanilla tools as possible. Construct comes with tons of tools already, may it be for organising them, commenting, or just plugins that come with the software.
These tools and plugins are working properly and supported. They might lack a few features sometimes, but again, try reducing the amount of 3rd parties to a vital minimum.
- The events should make careful use of families, unattached event sheets, inclusion, groups and functions.
This may seem obvious to most of you, but these are extremely important in making proper code.
How do we do that?
Construct didn’t invent a coding system. It is heavily inspired by how regular coding works, and thus optimal events should be heavily inspired by optimal coding.
Model View Controller
This is a common architecture in web apps. The main concept is as follows:
You have what is called a view. This is what the user sees and interacts with.
You have a model. This is the data and information useful to the view
You have a controller. This is what takes input from the view and modifies the model accordingly.
In Construct 2, the View is the layout, the Model is the set of instances on that layout, and the event sheet is the controller.
One thing we can do is divide the controller into modules.
For each type of action, or group of objects, a module will be attached and will manage everything about it. That way you find yourself with lots of smaller event sheets that each do a very specific thing. The main advantage of this is that you can remove everything from the project except that one module and it will still work properly.
The real talk
I’m not going to keep going through my mental process because that would be way too long and tedious and very boring. I just needed to explain parts of it, so you can understand where I’m coming from. Now I’m going to tell you what I ended up with, aka what I think is the most optimal way to write events in Construct.
First, everything gets divided in two groups
In Construct, plugins are divided in two groups: World and object
World plugins are plugins that can get instantiated and get placed on the layout.
Object plugins are plugins that are never placed in the world and don’t have instances.
Within the Object plugins there are two type of plugins, the plugins that are single global and the ones that are not. Single global plugins can only exist once per type and are usually plugins like Audio, Mouse of Keyboard.
There is one thing to note about the plugins that aren’t single global (apart from the fact that you can have multiple plugins with their type) is the fact that you can add them in the container of a world object.
Knowing this, we can try to apply this principle in our code as well.
4 types of event sheets
Following this, we’ll divide the event sheets in 4 types of event sheets.
Controller
Controller event sheets are the ones linked to layouts. They are the ones that call and include the other event sheets, and all the custom code concerning a single layout goes in it. Most of the time, they will contain next to no code and will be included in all of the layouts that don’t need custom code.
Module
Modules are like magic boxes. You don’t need to know what happens inside it, all you need to know is that they come with inputs, aka the module’s function API.
The functions should do a single thing and the output should be consistent.
Modules should be contained inside a group, and everything that concerns that Module’s name should be done inside the module.
If you follow this structure and name each subgroup of your module as
MODULENAME SUBGROUP SUBSUBGROUP, you make sure that no other group in the whole project will ever be named like this, and that no conflict will happen.
It also organises the Group names pretty well when you need to search for them using the Group expression field.
Each module should have an Init and an API subgroup (unless it’s really not needed).
Init should contain everything the group does when initialized, and API will contain every function.
The function names should also follow a similar notation: MODULENAME FUNCTION NAME
For the same reasons, this makes sure that no other function will be named the same, and that will allow you to find them much easily in the Function expression field.
You might also see the use of static variables in that module. Indeed, if you need global variables for your module, put them as local and static instead, that way you won’t be able to access them from outside the module, unless you specifically make a function to return its value.
Family
I’m not going to explain what a family is. I’m just going to remind you that an object can be part of multiple families at a time. Families don’t have inheritance but they don’t really need to thanks to this. While classes usually have a tree like formation, like this:
Families will have a different structure
Properly organising families means dividing them in the simples actions its members can do, and then build object types by adding them to every needed family, much like building bricks.
You can also simulate family inheritance if needed using this piece of code
This should only be used in preview and for debugging purposes, and the code will be removed automatically by C2 on export to save performance. With forced inheritance considered, the graph may look like this.
However, this inheritance is limited because the family doesn’t inherit the behaviors or instance vars from the parent. Instead if you need to do things with the parent’s vars or behaviors, you’ll need to pick the parent instance by UID (which will be effectively the exact same object but controlled from the parent family. Try avoiding this as much as possible anyway.
While it doesn’t work like classes, it can do things that classes can’t really do. The fact that children can have multiple parents makes it powerful in its own way. It’s just a matter of using different conventions for structuring logic and objects.
Now that the family structure is made, you need to write code for it.
We’ll be following a structure similar to the modules, but the API will be different.
Instead of using the vanilla Function plugin, I’ll be using Chadori’s Self Functions plugin. It allows world objects to have Functions attached to them, and essentially works just like class methods.
Event Bank
Now for the last type of event sheet. This one might not be useful to everyone and works more like a “Misc.” category rather than something precise.
Sometimes, you need to store things. Just like Object Banks exist in layouts, Event banks should exist in event sheets. What it can be used for will vary from game to game, and actually serves more purpose than Object Banks.
In fact, if you’re making an RPG, you may need to write the code concerning how your skills will play out, how enemy AI will work, how quests react, and how your boss will apply its patterns. These are necessary in some games, and in most big productions, and yet they don’t fit anywhere else: They are not custom layout code, they are not global modules, and they are not part of a family.
So, when you need to write some code that needs to be part of your project but can’t be included in any other event sheet, just write it in an Event Bank and include it where it’s needed.
There is one thing you want to make sure of though. You don’t want to write code that runs every tick, most of the time. Event banks will probably be the biggest event sheets and will contain lots of events, so make sure that each event is only used when needed. Quest data, Skills, Boss patterns and Cutscenes never need to be evaluated at each tick.
A good method to make sure that you are not evaluating useless code would be to put them inside group and have a module or function manage activating and deactivating the groups.
The case of 3rd party addons
3rd party addons are a great addition to Construct and will make your life easier in many ways. However, do not overuse them. They are still 3rd party and will probably not have the same support as vanilla plugin. If you run into an issue caused by a 3rd party that isn’t supported anymore there is nothing that you can do sadly. You don’t want this to happen during your big project, so keep your 3rd parties to a vital minimum and prefer using ones that are still actively supported.
Now 3rd parties may be very great when you are the one developing them. If you know the ins and outs of writing addons for Construct, do as you please. Just keep a rule in mind: Do not try to hack stuff into Vanilla plugin, or to produce badly crafted addons. You might be a great developer, and most of your addons will never get used by anyone but you, but you will run into issues if you don’t fully commit to making every plugin you make be at least good enough for someone else to use.
Side note, if you know how to make addons, that means that you know JS. If that’s the case, please don’t use the Browser Eval action. Evals are slow, and C2 doesn’t have syntax highlighting nor does it have big enough fields to write code in. It’s ok for short functions used occasionally, but don’t use them for anything important. Make an addon instead or use Valerypopoff’s JavaScript plugin which runs that much faster than Browser evals.
3rd parties can still be extremely useful and I’m going to talk about a few addons that will probably help you in your project.
Chadori’s Self Functions
Already talked about this, because it’s extremely useful to replace functions with UID picking. It makes the code a lot more readable and will allow you to write code that is even more organised while not loosing performance.
Toby R’s Gobals 1.0 and 2.0
Globals 1.0 is a plugin that does nothing. It runs no code, has no actions or condition. It can hold instance variables though. They will help you organised any global variable you have. There are many advantages to using Globals instead of regular variables. Globals 2.0 comes with tons of additional features that will make your life even easier.
Skymen’s Skin It
My skin it addon group (aka a plugin and a behavior) allows you to apply skins easily to any sprite you want. Skins stored into Skin groups and are divided into sub skins. The main purpose of the plugin is to create a skin or wearable equipment system easily, but the fact is that it also allows to divide visuals and colliders. You could write your player code on a Player Collider and have many player skins act as different characters in your game. You could also create enemy types and then have enemy instances have different skins, even though they’re the same enemy. That indirectly allows for real family inheritance, where you make families structured like detailed previously, and then have multiple enemy reskins be based on a member of that family. You essentially just derivated multiple enemies from one sprite that was inheriting family behaviors and instance variables. The only drawback is that this can only be done on a single level, so multi layered inheritance isn’t possible.
Making in house tools
That depends on the project and on the developer. Just keep in mind that most of the time you really don’t want to reinvent the wheel. If you can already do what you’re trying to do using existing and proven to work editors (most of the time, the Construct editor), use them.
However, making editors might be very good for your project and boost your productivity, so it’s up to how good it will be and how much time you can spend on supporting it and fixing the potential bugs.
Thing is, most of the time, you don’t really need an editor, and you can just use C2’s layouts and event sheets and work with them. That’s better because these are reliable, and you only need to find an efficient way to use them for your goal. In the end, it’s might be easier and faster to use than your tool.
Other notes
This section will be less about events and more about other practices I use in a project.
Organising Objects
When working with a big project, you will start to have to work with hundreds of objects, so keeping everything sorted will help you a lot. I like to have a folder named “_Core” that keeps every global plugin I need everywhere in my project. That way it’s always first in every object selector.
Using Global Layers
Global layers are a very cool tool to reuse UI elements across multiple layouts. Use it for objects that will be on lots of layouts and that need that layout to keep running under it. Refrain from using it for menus if your menu pauses the game for instance.
It is useful for combat UI, player HUD, dialogue boxes, etc. It is not useful for pause menus. Beware though as one drawback of global layers is that they need to be added manually in order everywhere. If you fail to do that even once that will mean that at one point in the game, and that point only, the UI will break. If you want more info on the drawbacks of global layers, look at my blog post Creating layers out of laziness https://www.construct.net/en/blogs/skymen-13/creating-layers-out-of-laziness-1027
Making an in-game menu
Try isolating your in-game menus in separate layouts with separated event sheets. That way you make sure that only the menu code is running and that inputs aren’t going to act on anything outside of the menu. That and the fact that using global layers for in game menus has drawbacks as stated in the previous part. If you want more info on how to do that, check my blog post Working around a workaround for more info on this and how to implement it https://www.construct.net/en/blogs/skymen-13/working-around-a-workaround-1034.
Managing inputs
Managing inputs is pretty important for a game, especially if you plan on porting it to multiple platforms. For that purpose, I created an addon (which I am not ready to share btw) that helps me manage inputs from mouse, keyboard and gamepad.
I recommend you to find a way to manage inputs similarly, because keeping every input somewhere helps making sure you can’t do weird things with your inputs, at least with other inputs that aren’t registered.
Using external files
Construct allows you to import external files, Construct 2 puts these files in the root folder of the export, so that means that any file is accessible via AJAX calls to ‘file.ext’. This means that any addon that can load a URL can load an external file, without going though the AJAX plugin. It can be useful to store static UI images and load them into a canvas from file instead of creating a sprite.
External files can also be used to store data in JSON or XML files. This is useful for stuff like dialogs, player and enemy stats, quest and item data if you have any, or even menu configurations. You can go wild with these file formats if you know how to use them.
These files could also be checked to allow for modding the game. AJAX allows you to check if a file exists and thus, you could have a JSON containing the path to mods and load them into the game. In NW.js you could even save and load files using a custom saving system.
Using AJAX in general
AJAX can be a powerful tool if used right. First, it can be used to check for GPDR on mobile games. All you need is to host a very quick PHP script on your server and have it return 0 or 1 to know if the IP you called with is subject to GPDR.
AJAX can be used to load levels and assets from your server dynamically. If you made a custom database system for storing images, text, variables, or anything else, you can use AJAX to communicate with it.
For big projects, it’s especially useful if your game has an online feature but shouldn’t require a full 3rd party database system like Firebase of Playfab. Just make a custom database, write some PHP and do AJAX calls. Will be a tad harder, but you’ll have full control over what happens and you won’t need to pay for a service you’ll barely use.
Conclusion
To conclude this very long blog post that took me a few weeks to write, making a big project in Construct requires optimal eventing. Optimal eventing in Construct requires adapting yourself to the tool and exploiting what you have to your advantage as much as possible. So, use Families, containers, groups, folders, global layers, and addons properly and organise it all into bite sized systems.
You might have different ways to organise stuff and might not agree with some of what I said. In that case, you are encouraged to share your opinion in the comments. In any case, thank you for reading through all of this, and I’ll be back in the future with a new blog post.