Dev Blog October 2026
Beta 2.0
I'll start this dev-blog by announcing that the beta test for 2.0 is open, You can find it in your game properties > Game Versions & Betas > Beta 2.0
Contrary to previous beta-test, this one has no risk of save corruption, so you can safely play on 2.0 and go back to 1.1 at where you were at (which is why this beta is much more open than previous one)
2.0 Beta 1 - Changelogs
Major Feature - Coop
-
Added Online co-op via Steam, up to 4 player per lobby.
-
For now quite experimental, expect quite a lot of bug for now* (and that why this beta exist)*
Major Optimisation
- Performance gain up to x20 in late game
More details below in the devblogs
Modding 2.0
- There is now only a single version of the game
Game is no longer cut into Vanilla & Modded, now "vanilla" will handle both, reducing game's size on disk, but also allowing modding to be much more performant
- Data and Lua mods with an in-game content editor and node-graph effects.
this can be accessed in the Modlist on main menu,
-
Mod weapons, enemies, bosses, avatars, events, zones, game modes and translations.
-
Mods sync in co-op.
Completely experimental and untested
QoL
- Improvement to the monster healthbar visual
Shape is no longer rectangular
Added marker to give a rough idea to the monster health
-
Improved Player's healthbar visual and marker to more properly follow the frame
-
Improved the card tree to become much more readable
-
Bind to change card selection now work everywhere, and a small prompt is written to update you on the card selection mode you have
-
Profile menu no longer freeze when there is a lot of profile
Profile information are cached when possible, and the profile button instantiation is done overtime instead of all at once
-
Many Gamepad navigation fix in main-menu
-
I fixed everything I could experience with trying to move everywhere, it's likely not perfect, but I couldn't stuck focus anyways
-
Many gamepad navigation improvement
Page change with RB/LB should work with almost everything now, an the focused element should stay at the same place
- **Soul-card now slightly tilt depending on where you put your mouse **The intensity can be tweaked in options
Save format changed
- Changed profiles (and achievements) save format and save name
This basically mean that they take MUCH less space on disk, about -90% of space taken
Persistent save and achievements name has changed on 2.0 so they don't overwrite any save from 1.1,
You can safely try version 2.0 without fucking-up your saves
- Improvement to back-up conservation
Until now, every hours, a backup were saved, and basically never deleted, slowly taking more and more space on disk
Now back-up are gradually deleted overtime, the exact rule is :
3 Newest hourly backup
6 Newest Daily backup
9 newest weekly backup
1 back-up per month
From some test with long running profiles, it reduced back-up count from 140+ to about 14
- Changed Run save formats
Greatly compressed run save formats to take less space on disk
Run save now record the soul-shop upgrade when the run started and restore it on loading.
Changes
- Knockback is now based on where the damage is coming from
Knockback no longer always apply from player's direction (this may be reverted for single-player depending on feedbacks)
- Altar no longer block the player around it
Leaving the charging are will not reset camera back to player and allow player to activate another altar as if he wish
Objective shows one line per activated altar.
- Soul-shop upgrade no longer reset run's save
Thank to run saves now storing soul-shop upgrade inside the save)
- [b]Change Vine of Time slowdown-formula from
45-15/level^0.5to50-30/level^0.75[/b]
Fixes
-
Logs spamming no longer cause lag until console is open/closed
-
Console controls are no longer ultra buggy, and auto-completion work perfectly
-
Catched a lot of bug related to Bard's score calculation (but also many artifact using the wrong bard score for it's effct)
Also prevent infinite score
-
Dual Gatling not applying it's weapon modifiers
-
Silkskin artifact description mixing up "multiplier" and "base" name for dash charge and dash speed
-
Monster's RNG state not being properly saved leading
-
Monster healthbar not properly displaying "burn" portion of their health (the part that is gonne be lost over time due to current burn/poison)
-
Timescale sometime not being properly reset after a run restart
-
Zone Modifiers not being properly cleaned up
-
Modified saving to be transactional to avoid chance of save corruption
-
Corrupted save's stream no are longer kept open
basically it should no longer prevent the save file from being overwrite when corrupted)
-
Loading backup for achievements was checking if the wrong file existed
-
Cursed card drop from events being slightly off
Card with missing requirement could still be obtained
-
Katana missing one enemies in the targetable range
-
Minor Altar and Shrine placement issues
-
Sometime error about Difficulty ID causing run start/load to not work for no reasons
-
Restarting a run in survivors mode no longer break the music
-
Some issue about target position for orbiting enemies being tied to framerate instead of time
-
fixed worm boss dash telegraph staying after boss
I really hope it is fixed this time <.<
- Modified how artifact selection menu slowdown is applied/removed
Now handle error during artifact selection to avoid time freeze after artifact selection
-
Stage can no longer generate with no path into it
-
Issue with Blood transmutation when Power was higher than the Maximum health, causing the game to freak out
-
Resilience causing massive bug for XPgain between 0 and 100%
-
Spirit ascension applying stronger effect than it should
-
Tactical defense description being very wrong, and effect actually not scaling with card level
-
Pride effect applying ten time more than it should
-
Blood spirit description having healing ten time stronger than the real effect
-
Rune of alacrity's description being off by -12.5% (the effect is actually 12.5% stronger than it is written)
-
Concentration description being off by -10% on the damage increase
Game Optimisations
1. ECS Migration
This is the biggest work done so far, this completely changes how projectiles are handled by the game. Instead of having one GameObject and one script (that was still partially batched) per projectile, the game now puts all projectiles in an array of Entities, which have different Components. Multiple Systems dictate their behaviour based on what they are.
This allows projectiles to be super lightweight where before, creating a projectile meant creating a GameObject, a sprite, a visual effect, a script that had a lot of data contained inside (which was often heavy, like the statistics class).
Additionally, ECS allows projectiles to be moved in batches (multiple at once) and makes multithreading much easier to implement. This has provided huge performance gains overall.
Before I go deeper into the explanation, I'll explain a few things. Classic Unity development leans heavily on what is called "Object-Oriented Programming" (OOP), which is a programming approach where everything is given a class. In OOP, we can also create a child class that inherits properties/parameters/functions from its parent class (for example, in Rogue: Genesia, I have the class "DefaultProjectile" which has a lot of functions/parameters to handle the default forward movement, but I also have a lot of child classes like "BoomerangProjectile" that change how the projectile's trajectory behaves). This has the advantage of reducing a lot of repetitive tasks by creating functions/variables that will be common (like time-scale statistics, fractal behaviour, level of fractal, whether it is critical, what happens when it dies, how to handle fading...). The main drawback is that it tends to create a sort of super grandmother class that almost every class inherits from, and you often end up with a lot of data/functionality that simply pollutes your class (along with a few issues when trying to share behaviour between 2 very different classes that should behave in the same way at certain points...).
ECS, and more importantly, Data-Oriented Design (DOD), is much more flexible, you can easily give the same component/tag to very different entities so they are affected by the same system. Another advantage of DOD is that it tends to pack data tightly in memory, which allows the CPU to read it much faster, makes bulk operations much easier to do and to parallelise (and also has some very beneficial things for modding, but that will come in a future devblog).
To do such a migration, I had to create a new way for projectiles to be created. Until now, creating a projectile sent a command to the projectile manager that said "hey, create me this projectile, at this position, with this orientation, and these parameters". The manager then checked if there was an unused projectile GameObject available. If not, it created a new one, then created a new projectile class instance of the requested projectile type and linked the GameObject. Then it added the class instance to a list of projectiles that the projectile manager was handling.
Now, the weapon creates a class instance called "ProjectileSpawnBatch" which is basically a command used to create a certain number of projectiles all at once. It then fills in the command details for each projectile one by one, such as their position, orientation, type of projectile, and all of their stats. Once the command has all the information, it is then validated and an entity is created for each projectile with the right components and data, and marked as "alive". At this point, the different projectile systems will handle their lifecycle (moving / checking collisions / checking the projectile's end of lifetime).
2. Separation between rendering and simulation of projectiles & presentation optimisation
Since projectiles are no longer GameObjects but "simple" entities, that meant it was a good opportunity to separate the visual presentation of projectiles from their simulation.
With 2.0, projectiles are displayed up to a certain cap (usually 16K projectiles). Beyond that, they are invisible, but continue to deal damage. I estimate that at this point, the visual noise would be so great that new projectiles are not worth the performance impact they could have. This cap is per type of projectile, and enemy projectiles have their own cap. I doubt enemy projectiles can reach their cap at all.
Another major change was a big optimisation pass on the presentation. Instead of each projectile having its own VFX instance, all projectiles of a type share the same VFX instance, and their visual effects (like trails) are presented as separate VFX. This has many huge advantages:
-
Massively reducing draw calls, since the CPU only needs to send a single draw call per type of projectile.
-
Massive reduction in CPU->GPU data transfer, it is much, much cheaper to send all data in a single batched command than to send 2500 commands.
-
This also allows a cap to be set on VFX without hiding projectiles. I've set a lower cap on the VFX that go along with projectiles, around 1K projectiles. This helps reduce overdraw (which is another issue that can slow down performance).
One last optimisation that has been done is baking some of the particle-heavy FX, like the Arcanic Beam, into a texture. This allows us to seriously reduce overdraw and cut out 99% of the VFX, with a result that looks 98% the same (also, the low-quality Arcanic Beam is no longer an ugly ray of light).
3. Physics Engine Migration
Migration from OOP to ECS caused the default Unity 2D physics I was using to become a major bottleneck. Only Unity's 3D physics supports ECS, which means I either had to revert to Unity 3D physics (which is not optimal for performance) or have GameObject proxies (which is exactly what I wanted to avoid by migrating to ECS...) for physics collisions.
After searching around, I found 2D Physics Core, which is a very bare-bones but very efficient physics engine that is made to handle entities very well (using a wrapper around the API).
In addition to being more performant, it allows multithreading with jobs to reduce processing on the game thread a bit (however, it is not totally multithreaded as a good part of the game is still dependent on the results of the physics queries).
4a. Damage processing optimisations
Next, the major performance bottleneck was simply damage processing, especially in the late game. Each projectile collision with an enemy created one damage application that was, in most cases, processed immediately. During processing, it would calculate defence and defence piercing, then apply different card effects that scale with hits, then apply damage, do a death check, and finally validate the kill. In some extreme cases, the number of damage applications could reach up to 1 million in the late game, and while defence/defence piercing/damage are relatively cheap to calculate, effects from cards can quickly become heavy on performance, especially at such a scale.
My first step was to put all the processing at the same moment (at the end of the frame), then only process the first 5 damage applications per enemy per damage source individually, and batch the rest into a single batch. This change alone reduced damage applications in extreme cases by up to 90% with very minimal changes to the accuracy of the simulation (which is more precise anyway because of the higher framerate).
Now that all the damage processing was done at the same time, the next step was to try to multithread as much of the damage processing as possible with the help of jobs and look into moving to a more performant solution for the rest of the processing thanks to Burst.
4b. Burst migration
This is one of the best ways to optimise hot paths. Burst compiles C# code ahead of time (AOT) into highly optimised native CPU code. The main issue with Burst is that you cannot do as much as you usually would with C#, only a small part of C# is supported, and it doesn't support any classes or managed types (like strings, Lists, ScriptableObjects or even GameObjects). That also means that memory has to be manually handled, making it harder to work with (but it also means no garbage collector).
However, the gain in performance is really, really good, it is also highly suited to multithreading (which is perfect for the damage processing issues). It is up to about 5x faster in terms of single-thread performance (30x compared to Mono) and thanks to multithreading, combined with everything above, an extreme late-game scenario went from 1-3 fps to 40-50 fps .
5. Moving from a dynamic tick rate to a fixed 60 ticks per second
This is more of a result of migrating to ECS than a proper optimisation, but it is still an important thing to mention. Now the tick rate of the game no longer depends on the framerate, which means that having 15, 30 or 60 fps no longer affects your maximum attack speed or how quickly enemies are hit. I originally went with a dynamic tick rate (scaling with the framerate), so heavy builds would nerf themselves and reduce the load on the CPU.
With 2.0, performance is seriously good now, good enough that I can restore a fixed tick rate without causing the framerate to spiral out of control. There are still super extreme cases where it can happen, but those situations should now be the exception (with players intentionally trying to make the game lag rather than being affected by the lag). In addition, the number of ticks per rendered frame is limited to 4 at most to avoid the game lagging too much, at the cost of slowing down the simulation.
What's left to do on optimisation?
There are still a few minor things I left out of the list (like huge optimisations to the amount of memory allocated for GC), but those are the major improvements and the impactful decisions that went with optimising 2.0. But the 2 biggest things are to continue looking at the remaining overdraw (and I think Thunder Vial is a very good candidate for that), as well as making sure that the HUGE refactoring those optimisations required did not introduce new bugs.
Комментарии 0
Гостевые никнеймы не подтверждают личность. Все комментарии проходят проверку владельцем сайта.
Обсуждение ещё не началось.