Construct 2's event system is designed to be easy to use for complete beginners, not requiring any technical knowledge. We regularly talk to users via our email support and the forum, and we've started to notice patterns in a few event features that are regularly misunderstood or used incorrectly or unnecessarily. There are also a few problems people run in to due to a basic lack of appreciation for how computers work, such as the fact they do not have an infinite amount of memory and processing power! Here are a few examples with some deeper explanations - try to avoid these if you notice them in your own projects!
Redundant 'Every tick' condition
Technically, the Every tick condition just means 'true'. The way events work is that they run the actions if every condition is true. Therefore, adding Every tick to an event with other conditions has no effect. The event below shows a pointless Every tick condition:
It is best to design your events to be as concise as possible, so the condition should be removed:
The event functions identically, and is simpler.
The reason the condition is called Every tick is because event sheets are run once a tick. An event containing just an Every tick condition by itself will indeed run every tick, so the description is accurate and usefully indicates the intent. However when used redundantly as in the above example, it's a little misleading, since the actions definitely won't run every tick!
It's worth noting an event with no conditions also runs every tick, but this could look a little mysterious to a beginner, hence the existence of a condition for the purpose. Our advice is to use 'Every tick', unless used in a sub-event to simply run some actions after another sub-event, in which case an event with no conditions is more suitable. The example below demonstrates this: the event with no conditions is simply used so that the Go to Layout 1 action always runs after the player count has been checked. This also avoids suggesting the action might run every tick.
Unnecessary use of sub-events
Sub-events are great for defining more advanced logic in your games. However we commonly see them used redundantly. Sub-events inherit the picked instances from the parent event's conditions, and often a sub-event can simply be merged with the parent event. Here are a few examples:
In the above event, there is no need to put the Player.Count = 0 condition in a subevent. It works identically to having it all in one event, as shown below:
We recommend this approach since it is easier to read. Indentation makes reading slightly more difficult. Additionally the use of a sub-event can imply it is necessary for some reason (since it wasn't made as a single event), which can mislead or confuse other people (or yourself) when reading events. This is especially important when teaching students, since they may ask why the sub-event was used, and there is no good answer.
Some users may take advantage of sub-events to help organise events (since they can be collapsed and expanded). This is fine - but we recommend preferring an easy-to-read approach, and then re-organising to use sub-events later on if necessary. Until then, why not leave it in a style that's easier to read? And of course there are many cases where sub-events are truly necessary, such as to eliminate repeated conditions or when actions are used in the parent event. There is no problem with that; it's just that if it would work the same if not using a sub-event, we recommend not using the sub-event.
One more common example of unnecessary sub-events is when using nested loops, as shown below:
This is probably arises from thinking about traditional programming languages, where typically a nested loop is indented inside the other loop. However in Construct 2 it's not necessary, and nested loops can in fact be placed in the same event and it works identically:
Another minor point is the Construct 2 engine sometimes cannot treat sub-events as efficiently as normal events. In rare cases, avoiding unnecessary sub-events could improve performance, especially when loops with lots of iterations are used. Still, the readability is probably the more significant concern.
Unnecessary 'For-each' loops
The System For-each looping condition is useful in lots of circumstances, but it is often used where it makes no difference. Going back to How Events Work, conditions are tested for each instance, and then actions are run for each instance that met the event's conditions. As that sentence suggests, the Construct 2 engine already has built-in for-each loops to help events run as you expect. For example, consider the following event:
Construct 2 already runs the event like this: for each monster that is hunting, set the angle towards the player's position. However we often see events that look like this:
In this case the use of the system For each loop is redundant. It shows a misunderstanding of how Construct 2 events already work. It does not change the meaning of the event at all - the engine was already doing that. Again, there are two disadvantages to this kind of event: mainly that it is confusing to include events that have no effect, but also performance is a bigger concern in this case. The engine's own for-each loop happens directly at the javascript level, and is fast; for-each loops happening at the event level involve the overhead of the event engine, which while we've worked hard to optimise, should be avoided if unnecessary.
So when should for-each loops be used? Only when what the engine is already doing is not enough. This is usually when you want all the event's actions to run once per instance, rather than just the actions relating to that object. Often this is to get system conditions and actions to run for each instance, which are normally only checked or run once (since there is only ever one system object). For example, if the above event included a system action to subtract from a variable, adding the for-each loop does then change the behavior of the event. Without the explicit loop, the system action only subtracts from the variable once when the event is run, regardless of how many Monster instances were picked by the conditions; with the explicit loop, it also runs the system action once per picked Monster instance, meaning the amount subtracted is proportional to the number of Monsters that are hunting. This can certainly be useful, but the cases when it is truly necessary seem to be less frequent than many users think. Often what the engine does already is enough.
Using 'dt' in 'Every X seconds'
It's great that many users have read our tutorial on delta-time and framerate independence and are using the techniques in their projects. However attempts to use dt (delta-time) in the Every X seconds condition is a misunderstanding of framerate independence.
An event like Every 1 second is already framerate independent. It measures time, not frames. It runs once a second regardless of the framerate. At 10 FPS, it runs once a second. At 60 FPS, it runs once a second. There is no variation with the framerate that needs to be compensated for.
An event like Every 60 * dt seconds is suddenly framerate dependent - the opposite of the goal you are trying to achieve! At 10 FPS, dt is 0.1, thus the event runs every 6 seconds. At 60 FPS, dt is about 0.016, thus the event runs about every 0.96 seconds. It varies with the framerate, which is what we want to avoid.
On the other hand, moving an object 1 pixel every tick is framerate dependent, since the speed the object moves at depends on the framerate: 30 pixels a second at 30 FPS, and 60 pixels a second at 60 FPS. Here, as the tutorial above describes, dt can be used to move it at a constant speed. dt is the time a tick takes, so by moving an object 60 * dt pixels a tick, the object moves 60 pixels a second at 60 FPS, and 60 pixels a second at 30 FPS. This is the real purpose of the dt expression. Don't use it with things that are already framerate independent.
Using small values in 'Every X seconds'
Another bad practice is to use very small values of time for the Every condition, such as Every 0.01 seconds. The target framerate of most games is 60 FPS, where the events are running about every 16ms (0.016 seconds). The Every condition is checked every tick, and can only run at most once every tick. Therefore if the time interval is less than 0.016, it will simply be true every tick. Even if the framerate goes down to, say, 10 FPS, ticks are now taking 0.1 seconds and still the condition runs every tick. So such a condition acts identically to the 'Every tick' condition. And, as described above, 'Every tick' is itself often redundant and can be removed. So if you have an Every condition using a time interval less than 0.016 seconds, you can probably just delete it without affecting anything.
Pasting data in to expressions
It's almost always a bad idea to paste lots of data in to the Parameters Dialog. Sometimes the contents of an entire file is pasted in to an expression. It ends up looking like this:
If you are doing this, use project files instead. It is almost always better to paste this data in to a project file, import the project file in to Construct 2, then use the AJAX object's Request project file action to load it. What's wrong with pasting lots of data in to expressions? It's bad on several points:
- All the pasted-in data has to be updated to match Construct 2's expression format to avoid a syntax error, such as requiring two double-quote characters in a string literal to result in a single double-quote character. This is tedious when using formats like JSON which commonly use double-quote characters.
- It's difficult to find, read, and update all the data from the Parameters Dialog.
- It can make the event sheet extremely long as it displays all of the data, requiring lots of scrolling past.
- All the data ends up in the exported c2runtime.js file. This must be fully downloaded, parsed and loaded in to memory before the game can even start loading. If you are pasting in large data files, the download size is increased, the startup time before the progress bar even appears is increased, and the memory use can be increased (especially problematic on mobile).
- It can even make projects take longer to open and save in the editor, since the pasted-in data is stored in the project XML, requiring XML parsing and conversion on every load.
Project files do not have these deficiences:
- There is no need to change the contents of project files to avoid syntax errors
- It is easy to find, read and update project files in a folder-based project
- It keeps the event sheet short, since they are loaded by a single AJAX request action
- The data is kept out of c2runtime.js, making it quicker to start up and use less memory, and only being downloaded when actually used, and are still cached for offline use
- The data is kept out of the project, making it quicker to open and save in the editor
If you are pasting long strings in to events, we strongly recommend using project files instead.
Expecting math calculations to be exact
All modern computers store fractional numbers like 0.5 in a floating point format. If you want to learn more about it feel free to follow up the link, but we won't be detailing it here. The main point is because computers don't have infinite processing power and memory, they must sometimes slightly round results. This in turn can cause the result of a calculation to be slightly off its true mathematical answer, such as getting 0.999999 when you expected 1.
To illustrate why this happens, consider dividing 1 by 3 with limited precision. The answer is of course 0.333333333... recurring forever. However computers have a limited amount of memory and can't possibly store an infinitely recurring number like that in full. Therefore they reserve room for a fixed number of digits, then stop. For example it might calculate with six digits and come up with the answer 0.333333. This is mathematically wrong, but your computer cannot possibly store the "right" answer, so it has to make do. Then suppose you multiply this result by 3 again. You'd expect three thirds to equal 1, but in fact you get 0.999999 - close to the right answer, but not exact. If your game expected the answer to exactly equal 1, you might find it's not working like you expected. You can usually inspect your project with the debugger to see the real answer that was calculated.
This type of error affects all floating-point calculations in almost all software in all modern processors. There's no escape! It's just a fact of how computers work. The real details are slightly different - my example used decimal (base 10) whereas the actual representation is in binary (base 2), and recurring decimals work differently in base 2. For example the number 0.1 is exact in base 10, but is a recurring decimal in base 2. This means rounding can happen in unexpected places, even if you think you're dealing with exact numbers. It's particularly likely to affect object positions, since moving an object at an angle involves calculating sin and cos, which will usually produce results close to but not exactly the true answer.
The workaround, which you must use with any software and any framework, is to allow a small tolerance. Don't expect an event like "Sprite X = 100" to ever be true. Instead use something like abs(Sprite.X - 100) < 0.01. This allows the comparison to be true so long as the result is within 0.01 of 100, so if it works out to 99.999999 your event still runs as expected.
Extreme pathfinding
The Pathfinding behavior can be very useful, but it's also one of the most CPU-intensive behaviors other than Physics. Finding a path is a very complicated operation which often involves trying many thousands of different possibilities before identifying a good path - and that must be done for every single instance every time the Find path action is used! Luckily it can be finding the path in parallel to the game running (thanks to running the path calculation in a Web Worker), so finding long paths doesn't hang the game. However you can push it too hard: some users naively set the cell size to just a few pixels big, or even to 1 pixel big (perhaps in an effort to get "pixel perfect" pathfinding). This is like trying to plan the fastest route to work atom by atom, with a list of directions entailing millions of impossibly small steps. Unsurprisingly, your computer does not have an infinite amount of processing power. Trying to do this will simply give the CPU more work than it can reasonably get through, and then you may find On path found becomes very slow to fire, or effectively stops triggering, particularly on weaker mobile devices.
The solution is simple: use the very largest cell size you can possibly get away with. The larger the cell size, the fewer steps are necessary and the faster the paths can be found. That's why it uses a grid in the first place - it's an optimisation, to calculate a useful path far faster than it can do when it has to worry about every pixel.
Regenerating the obstacle map is a similarly expensive operation covering the entire layout, and should be done as infrequently as possible. And as with using a tiny cell size, trying to find a path every tick - or as soon as the last path is found - will likely cause similar CPU burdens that cannot be worked through. Use these options sparingly.
Conclusion
Hopefully this blog post will help you avoid common bad practices in the event system, as well as common pitfalls from misunderstanding the capabilities of computers. Another common mistake is to assume computers have unlimited memory, which we previously wrote about in the blog post Remember not to waste your memory. Read through that and the other linked tutorials if you want to go in to further detail, and remember we have a lot of tutorials and a comprehensive manual to help you along the way. Happy Constructing!