Yes, if you were wondering why Officeworks no longer seems to have any erasable pens in stock, now you know why
Yes, if you were wondering why Officeworks no longer seems to have any erasable pens in stock, now you know why
Seriously, I am pushing my brain to the edge, trying to imagine would your particles need 90+ Alt. Values.....
Well, take twirling for example, which is a movement that kicks in semi-randomly:
When twirling begins for a leaf (if it's marked for twirling in the first place, that is), we first need to create a fulcrum around which it'll twirl and store its ID, so that each leaf/fulcrum pair will be correctly 'married'. We must figure out where to put the fulcrum (which is normally invisible): so that's the fulcrum's direction in relation to the leaf, plus the radius at which the leaf will twirl around it. We determine how wide the radius should be by taking into consideration a few factors, including how fast the leaf is travelling and how close it is to the player; we calculate these into variables we call the speedFactor and layerFactor. We also need to decide how many revolutions around the fulcrum the leaf will make, and we need to keep track of how many percent of its twirling journey have been completed. We calculate that by keeping track of the leaf's starting direction, finishing direction, and current direction.
The leaf is always spinning, but we give it a little bit of extra spin when it twirls. We also give it some extra scale while it twirls, to add the illusion of it twirling towards the camera a little. And we also give it a little extra speed during the twirl. But we can't just abruptly add extra spin, scale and speed when the twirl begins, and then abruptly take it away when twirling ends - that would look broken. So we need to gradually increase/decrease the spin, scale and speed, by a separate acceleration value. Now, for the twirl itself, we need to figure out how to actually achieve that - in other words, what modifications we need to make to its X and Y position. We do that by taking the leaf's current twirl angle and performing some trigonometric functions on it, which gives us the X and Y values we need, which we call twirl X and twirl Y. We can then add these to the leaf's main positionX and positionY values, so that the current portion of the twirl will be included the next time we reposition the leaf.
However, we don't want the leaf to perform perfectly circular twirls, as that would be unrealistic. Instead, we want it to to twirl in an ellipse that is pointing in same direction that it's moving. To achieve that, we need to somehow convert the leaf's 32-directional value into X and Y format so that we know how much X and Y movement to apply to create our ellipse. There's probably an easier way to do this with trigonometry, but the way I do it is by setting a variable called xDirectionfactor2way to Abs(8 - Abs(Dir( "Group.Particles" ) - 16)) * 0.125 and then using it to set another variable called xDirectionfactor4way to ( ( Abs(Dir( "Group.Particles" ) - 16) - 8 ) / Abs(Abs(Dir( "Group.Particles" ) - 16) - 8) ) * xDirectionFactor2way( "Group.Particles" ). And of course I do the same for yDirectionfactor2way and yDirectionfactor4way. Though unwieldly, these equations produce two simple values: how much X movement and Y movement is involved in a particle's trajectory, no matter its direction (so if it's moving straight up, X is 0 while Y is -1.0; if it's moving diagonally right and down, then X is 0.5 and Y is 0.5)
So our twirl is done. However, we'd like to modify it depending on why the leaf is twirling. If it's just twirling on its own, then this is fine. But if it's twirling because Spryke just collided with it, then we want to give it some more turbulence, so we transfer some of Spryke's movement speed into a variable called extra twirl and use that to make the twirl faster, smaller, and with more revolutions. If, however, the leaf is twirling because the wind has picked it up, then we also need to adjust the elliptical direction of the twirl to be more in line with the direction of the wind (if the leaf is travelling downwards but the wind is blowing it sideways, the leaf should twirl mainly sideways, not downward), so we also need to factor in the windXspeed and windYspeed
So there you have it. The 27 or so bold words above are alterable values. And that's just for twirling. There's also pivoting, spinning, wobbling, scaling, veering, opacity, lifespan, rgb, and splashes to worry about. Like I said, this was one of the most complicated things I've ever done. I'm very happy to be finished .
OK, now this was complicated as hell... But is totally clear... Thank you for even bothering writing it down .
The result is very fine by the way.
Sir Vol, this looks absolutely amazing. How in the heck does Spryke run at 120+ FPS. My game runs at a much lower resolution and grinds to a halt when I start generating lots of particles (fastloop-based custom physics engine). I feel exceedingly inadequate in the Fusion. department.
Please show your working. I think you're cheating!
Well, each particle uses the bouncing ball movement, which is faster than manually moving things, and I don't have to worry about coding in events to make speed and 32 directions work. I also try and figure out as many necessary calculations as I can during the creation stage and store them as altVals, so that Fusion won't need to calculate them repeatedly during the execution stage. And once all particles are created, I run a loop that assesses each of them to see how often they need to be updated for various attributes. For example, if a particle ends up with parameters that will make it spin, but not very much, or it's very small, or far away, then I'll flag it as updateAngleSlow, so that Fusion only has to update its angle every once in a while. Here you can see that certain things, like updating the RGB coefficient, I only do about 3 times per frame (every 41 ticks). Others I do every 7 ticks, every 5 ticks, every 3 ticks, etc. as needed.
Doing different things at different tick intervals also helps spread the load more evenly, so I'm not burdening Fusion with a huge workload in one particular frame, which might end up causing a spike that creates noticeable stuttering. That's also why I use every 41 ticks rather than something more regular like every 40 ticks. 41 is a prime number, which means that if I set certain things to be done every 41 ticks, they should end up being done on their own more often, rather than frequently at the same time as those things which are set to be executed every 2, 3, 5, 7, or 10 ticks, for example. For instance, every 30th tick will always coincide with every 2nd tick, every 3rd tick, every 5th tick, and every 10th tick, since 30 is divisible by all those numbers. But every 41st tick will only coincide with every 2nd tick 50% of the time (41, 82, 123, 164...) and every 5th tick only 20% of the time (41, 82, 123, 164, 205, 246...) and so on.
In case it's not clear, the way I can tell when it's every X ticks is by setting up a little system of altVals that makes use of the mod function:
Wow, this is so true. It's only a few months after I finished this particle engine, and I've re-read the post below, and I can only barely understand any of it! The human mind is amazing - both in its capacity for complicated problem-solving, and its ability to turn around and flush all that knowledge down the toilet once it thinks it won't need it anymore!Originally Posted by Volnaiskra
Just a quick update as I haven't written anything on this thread for ages.
I'm currently working on an enemy engine now (collision, pathfinding, shooting, etc.) In the past, I've coded each enemy type on its own, in an adhoc manner. That's worked fine for PAX demos, but ended up being way too flaky for the real game: enemies quickly break when I put them in different scenarios than the test levels they were intended for, and I end up wading through separate piles of spaghetti code for each enemy that aren't consistent or well-connected. And as I've learned over and over again, every time I get a little lax in making my code simple and modular and well-commented, I end up paying for it later, when it takes me a whole day to change one little thing because I can't remember how the code works. And this section of the engine is particularly ripe for complication, so I'm making my way through it slowly and cautiously.
I'm working on a universal enemy system that will work as much as possible 'out of the box' with any enemies that I put into the game. The idea is that I'll be able to add many different enemy types as I go by just adjusting a few parameters, without as little extra code as possible. So I can set their speed, acceleration, whether they shoot, how fast they aim, whether their bullets spray in a burst (if so, how big is the arc), are the bullets homing, do the enemies chase Spryke or run away, can they crawl on walls and ceiling or just the ground, can they try and jump up at Spryke when she's above them in the air, etc. etc. And the code will do the rest, enabling the enemies to navigate the environment, jumping over gaps, avoiding hazards, dealing with slopes, trying to predict where Spryke will go when chasing, trying to break out of repetitive loops when they happen, and so on.
It's pretty excruciating. It feels like every time I successfully implement a feature or fix a bug, it just shines the light on some other thing that I didn't think about. This is definitely one of those areas where, done right, it's almost invisible, but any flaws in the design are immediately apparent, because enemies break or behave stupidly. I always knew that this part of the game was going to be a lot of work, but even so, there's so much more to getting this stuff right than I'd realised. Like just take jumping over a simple gap. How long the jump will take depends on the size of the gap, but also on the speed of the enemy - a slow enemy should take a longer time to make the jump. And for the jump to look smooth and good, the engine needs to factor in the appropriate length and height of the jump, and also an appropriate level of 'squish' or 'bounce' at the end of the jump. And is the gap on the same level as the enemy, or is it a drop down, or a step up? And is there enough landing area after the jump, or will the enemy crash into a wall or a hazard, or off a cliff? And even if there's an adequate landing area, what about the air in the middle of the jump - is it clear, or is there an obstacle? And if the enemy is chasing Spryke, will jumping take it away from her? If the gap isn't jumpable, then should the enemy turn around, or can it walk on walls and/or ceilings? If so, it should first check if there is a ceiling it can jump to or a wall it can crawl onto....and in both of these scenarios it now needs to make many of the previous checks: is there an adequate landing surface, is there a hazard waiting...etc. etc. etc.
And of course, all of these things have to happen as efficiently as possible, with as few detection checks as possible at once, to keep performance running smoothly. So the engine tries to calculate how often to check for what. For example, slower enemies might only need to check for slopes every 4 ticks, whereas a fast enemy (or one that was slow, but has just sped up because it started chasing Spryke) might need to check it every 1 or 2 ticks or it risks getting stuck in the geometry. I try and only check for things when necessary, and I try to check 'broadly' at first. For example, when checking if a gap is coming up, I don't check for holes every single tick. And if I find a hole, I don't do a brute-force check of every pixel in a fast loop to get the exact location of the hole (move detector 1 pixel - is it overlapping now? Move detector another pixel - is it overlapping now? Move detector another pixel - is it overlapping now.......) Instead, I'll put a large detector in front of the enemy maybe every 10 ticks or so. If that detector hits a hole, then I send out 3 "mini" detectors, spaced apart. Depending on which of those detectors do/don't overlap holes, I'll send out 3 more "micro" detectors, more closely spaced. Once those are tested, I'll know the location of the hole to an accuracy of about 5 pixels. I then do cubic polynomial regression calculation (I didn't even know what that was a few months ago) based on the enemy's speed to try and predict where in that ~5 pixel range the hole is more likely to be. Now my engine knows with reasonable accuracy where exactly the gap lies, with fewer expensive overlapping checks than if I'd tested every single pixel. Once my enemy does whatever it is supposed to do with the gap (jump over it, climb down it, turn around) it will be in almost the right spot. And every enemy periodically checks for positioning flaws, so if an enemy ended up 2 pixels too far or to close to a surface, it will auto-correct within a second or so.
It's exhausting, and it's harder to motivate myself to do this stuff than it is to do the more fun stuff (like graphics). But I feel like I'm nearing the end of this enemy engine, though it's taken me a large chunk of this year (covid has slowed me down in a number of ways too - I've had less time to work and more distractions, with my wife needing to work from home and me needing to home-school my daughter when the schools were closed). So far, this enemy engine has 1000+ events, the main enemy object is up to Alterable Value FK, and uses a system of 70+ detectors! I have a very expensive bottle of wine that some friends bought me for my 40th birthday, which I've decided I'll open when I've finally finished this enemy engine. I hope I get to drink it before it turns to vinegar
Cool update @Volnaiskra ! Platform AI is really messy, cool methods you outlined.
I was reading your old posts and was curious of that library you talked about in an earlier post, did you build that viewer or is it a thirdparty?