Alright, I attached the map. However I'd still recommend doing it yourself since you'd be better off learning how it works than just copy/pasting it.
- Registered User
Member for 8 years, 9 months, and 28 days
Last active Thu, Dec, 11 2014 20:33:35
- 0 Followers
- 91 Total Posts
- 0 Thanks
May 31, 2011Posted in: Tutorials
Hi all, I'd originally posted this on my clan's forum but it's (no longer) out of service plus I'd like this to reach a bigger audience. I couldn't find any tutorial like this so I figured you'd like it.
A good understanding of triggers is recommended for this tutorial.
For my first Starcraft 2 custom map editor tutorial I'll go over how to implement simple physics and projectiles using triggers. I'll base it on the method I used in Magecraft, but this example will be for a Football map. Unlike Magecraft, however, it will involve a third dimension and possibly more. There's also minimal use of the data editor; I prefer to use triggers for the spells I make but there are certain things you can't do with just triggers, so I'll start by going through that. If you already know how to set up a unit with a point-targeted dummy ability then skip step 1.
It would be conducive for your learning to open up the map editor and try to replicate what I'm doing, in a new map or your own map if you'd like to add physics to it.
Some of the images have been shrunken by Photobucket, so if you can't read them try using the pop-out images here: http://www.clanttg.com/index.php?/topic/253-creating-a-physics-and-projectile-framework/#entry2427
Step 1. Data Editor
Let's start by making the Throw Ball ability. We can simply modify another ability, but we want one that targets a point on the ground. So let's modify MULE. Search for MULE in the ability menu of the data editor, and right click Orbital Command - Calldown MULE, then click Duplicate Ability...
Now we get the great wall of checkboxes. As it says at the top, each item checked will be duplicated as well, and each item unchecked will just be a link back to the old data object. We won't end up needing them anyway, so only check Buttons and Calldown MULE (Create Unit).
Now you can rename it by editing Ability - Name and Editor - Editor Prefix. You should now also edit the button you just duplicated by finding it in the bottom-left panel.
Now edit your duplicated Effect so that it doesn't actually spawn a MULE.
Now, go back to the original Throw Ball ability by clicking at the first item in the tree in the bottom-left panel. Let's get rid of the energy cost so we can use it as often as we please. Edit the field Cost - Cost +, then click on the first and only item in the list of the pop up menu. Scroll down until you find the Vitals section, where it shows Energy with a cost of 50. Edit that and set it to 0.
Ok, now the ability should work. The reason for creating a dummy effect is because some patch ago, Blizzard changed it so all abilities need to have an effect in order to register for trigger events.
But what's the good in an ability if no unit knows how to use it? Let's change that. Save the map and switch to the Units tab. I'm just going to make a duplicate of a Marine, with none of the boxes checked. After renaming it, edit the field Ability - Abilities +, click the little green X to add a new ability, and pick out your new ability (you can type in the first few letters of its name instead of searching manually).
Now, edit the Ability - Command Card field and add the button wherever you want.
Oh yeah, and one more thing. Your duplicated unit won't show up unless you also duplicate an Actor for it. So I just duplicated a Marine actor. Then, you need to go to Event - Events + and change every instance where it says the name of the original unit (Marine) to your new unit (Marine2).
Now go back to the Terrain editor, and go Layer -> Units. Search for your unit and place it so you can test it later.
Now you can test and see that it does nothing, as intended. This is called a dummy ability; it has no data editor effects but it will be used to create a projectile in the trigger editor. You may also notice that the unit doesn't stop or face the direction of where you cast. If you want to add that, you can do it easily enough in the trigger for the ability later.
Now let's create the football. By searching in the Units tab, I've noticed there's already a unit for the football. How convenient. You might want to create your projectile by duplicating that unit, or something similar. One thing bothers me, though, it's that the football is too big. It's about as big as the marine himself! I can change that easily by editing the unit's Model in the bottom-left panel.
Now, let's prepare for the collision detection by setting our units' Radius. This allows you to have larger units with bigger hitboxes acknowledged in the collision detection that we'll make later. I set my Player to 0.5 and my Football to 0.375.
Also make sure your projectile is unselectable and untargetable. It doesn't behave like a real unit so you shouldn't be able to select it or right click on it. Edit the field Unit - Flags and check Unselectable and Untargetable.
And finally, your projectile should not automatically collide with any other unit. Change its Movement settings so that it doesn't.
That's it for the data editor, so make sure to save your work. There's a few things you should understand before continuing, like physics basics and vector arithmetic. If this is already familiar to you, then skip to Step 3.
Step 2. Underlying Concepts
The math in the physics will be focused primarily on vectors. A vector is simply an arrow pointing from one point in space to another. Vectors can be quantified the same way points can, with a tuple of values (I'll use 2-dimensional descriptions for simplicity). For example, imagine the point (3, 2). There is also a vector (3, 2) which is an arrow from the origin to the point (3, 2). Likewise, if it's a 3-dimensional point, like (3, 2, 1) then the vector (3, 2, 1) points to it from the origin.
Do not confuse vectors with scalars.
Scalar - any number, like 4, 7.8, and -20.
Vector - a tuple of scalars, as described above.
There are 2 basic ways to represent 2D vectors: Cartesian and polar. What I've described in the first paragraph is the Cartesian representation, where there is an X component and a Y component. Polar is also a tuple, but it contains the magnitude and the direction. For example, (5, 90°) is a vector/point 5 units away from the origin at the 90° angle (where 0° is east, 90° is north, 180° is west, and 270° is south). It's equivalent to the Cartesian coordinates (0, 5). Later we'll write a procedure that takes either Cartesian or polar coordinates, and converts the polar coordinates to Cartesian.
The main engine runs on Cartesian vectors because they're much more efficient to do vector arithmetic on. Speaking of vector arithmetic...
Adding two vectors together is essentially taking the starting end of one and putting it on the finishing end of the other. So they form a path. But that path is irrelevant because the sum ends up being a single vector: the origin of the unmoved one to the finishing end of the moved one.
More literally, adding vectors is simply adding their respective components together. (1, 6) + (7, 4) = (8, 10)
Subtracting two vectors is similar. All you have to do is reverse the sign of the one being subtracted (make it face the opposite direction) and add them.
A basic way to find a vector between two points is to subract the components of those points (final - initial). To find the vector from (2, 7) to (-4, 3) just subtract the final minus initial. -4-2 = -6 and 3-7 = -4. The vector is (-6, -4). The vector from the second point to the first is simply the negative of the one we just found.
A vector can be multiplied by a scalar. For instance, a vector (2, 3) times the scalar 5 would result in a vector 5 times longer, or (10, 15).
Multiplying vectors has multiple meanings. The most useful for our purposes is the Dot product, which results in a scalar value. The dot product of Cartesian vectors (A, B) and (C, D) is A*C+B*D. There are many uses of this formula but we'll only need it for one purpose: to check whether two vectors are facing the same general direction. If they are, then the dot product is positive. If they're facing away from each other, then the dot product is negative. If they're perpendicular, then the dot product is zero.
Anyway, enough about vectors. For checking collision we'll use the distance formula, which is based on the Pythagorean Theorem.
Sqrt ( X^2 + Y^2 ) < Unit1.Radius + Unit2.Radius
As you can see, it's only when the distance between the center of two objects is less than the sum of their radii that they must be colliding.
Suppose we want these two objects to bounce off each other when they collide. First, it's sometimes necessary to instantly move them out of each others' collision range so they don't get stuck in each other. Then, we can apply this formula I found on the internet (http://www.wildbunny.co.uk/blog/2011/04/06/physics-engines-for-dummies/):
I = (1+e)*N*(Vr • N) / (1/Ma + 1/Mb)
Va – = I * 1/Ma
Vb + = I * 1/Mb
Where I is the "impulse vector", e is the elasticity where 1 = bouncy and 0 = not bouncy, N is the "normal vector" which is just the vector from one object to the other, Vr is their relative velocity (the velocity vector of one minus the other's), Ma and Mb are the object's respective masses, Va and Vb are the object's respective velocity vectors.
You don't have to understand how it works, just what each arithmetic operation means and how to implement it (which I'll get to eventually). * means vector-scalar or scalar-scalar multiplication, and • means the dot product of two vectors. -= and += mean subtract from and add to, respectively, but referring to vectors.
Anyway, that was fun, but now let's get to the real fun... putting it all together with triggers.
Step 3. Object Movement Framework
First, let's lay out some rules and variables.
There are two main unit groups: PlayerUnits and ProjectileGroup. ProjectileGroup is for all your projectiles and PlayerUnits is for everything else.
Each unit has an array of associated "Custom Values". These are incredibly useful for storing real numbers and attaching them to units, and allowing them to be globally modified. We'll use them to store the unit's velocity, friction, mass, and whatever else we need.
Custom Value 0 = Velocity vector X component
Custom Value 1 = Velocity vector Y component
Custom Value 2 = Velocity vector Z component (for arcing projectiles that need it)
Custom Value 3 = Friction Coefficient (where 1 = no friction, <1 = regular friction, and >1 = acceleration)
Custom Value 4 = Mass
Here I'm simply running a periodic event at every 0.0 seconds, which causes all the actions to run once every game loop (32 times per game second).
It dumps both of our global unit groups into a local one, then iterates over that. It causes each unit to be moved by its own velocity vector (that is, custom values 0 and 1). The Blend option should be enabled to make the sliding look more smooth.
Now let's think about the third dimension. That would be the unit's height off the ground. We can simply modify that by Custom Value 2 and then check to see if the height is less than 0, which means it went into the ground. If that's the case, then obviously it should be moved out of the ground, and the Z velocity should be made positive by taking its absolute value, and also multiply it by 0.75 so it doesn't bounce quite back to the same height. Note that this is designed for a flat ground surface.
Now we want to have it check for collision with other units. To do this, it's best to remove the picked unit from ObjectGroup so that, not only does it not attempt to collide with itself, but so that each unit checks collision with each other unit only once. Here we'll use the formula from the previous section: Distance between units < Unit1's Radius + Unit2's Radius. To get a unit's radius use the function Unit Property. I've also added comments to make things clearer as the trigger gets bigger. It's a good habit to get into, especially when writing difficult code and/or working with a partner.
Notice that I've added two things: an action definition called Collision and a function called Distance3D. The purpose of Distance3D is to find the actual distance between two objects, including difference in height, which the built-in function Distance Between Points can't do (it only considers 2D points). It uses the 3D distance formula:
Distance = square root ( x^2 + y^2 + z^2 )
Where x, y, and z represent the difference between the coordinate of each unit. More specifically:
Distance = square root ( (x1-x2)^2 + (y1-y2)^2 + (z1-z2)^2 )
Where x1 is Unit 1's X coordinate, x2 is Unit 2's X coordinate, etc.
Now let's look at Collision. It's an action definition, which are like functions but they don't return anything, and they can be called outside of expressions. Although I didn't actually need to separate it from the main trigger, it really helps organization and code reuse. Also, my editor tends to get laggy when working on triggers that get too big. What you want to do to handle collisions depends on your game. In Magecraft, projectile-on-projectile collisions destroyed them both, projectile-on-unit collisions destroy the projectile and damage+knockback the player, and player-on-player collisions did damage and knockback on each other. It also requires them to be enemies, so that there's no friendly fire. Let's start by separating these cases.
Before going any farther with collision, let's make it possible to fire projectiles first so we can at least test it. Perhaps being the greatest example of code reuse with action definitions, let's make two ways to apply a force: by a cartesian vector and by a polar vector. These help a lot when making all kinds of spells. I find myself using the polar type most often because a lot of my spells are based on angles. What they do is modify the unit's custom values, so that for example, if I apply an eastward force on a unit it should start sliding east. It also divides by the mass so heavier units require more force to move. It's not a perfect representation of physics, however, because in reality force causes acceleration, while this procedure causes a velocity. Really though, it causes a change in velocity from the previous game loop, so if you want to apply a force over a period of time, simply call this procedure over and over each game loop, which will make it accelerate. If you only want to apply an impulse, that is, a force for the duration of one game loop, call it once. Recall that custom value 4 represents the unit's mass.
The polar version does the same thing but receives a vector in polar format.
Now, we need to create the projectile when the player unit casts its ability. It's absolutely necessary to set custom values 3 and 4 (friction and mass, respectively) because a friction of 0 would prevent it from moving at all, and a mass of 0 would cause division by 0 errors. What I'm doing with custom values 0 and 1 (x and y velocities) is setting them to a fraction of their caster's velocity. That means, if the caster throws a ball while sliding, the ball will be slightly off-target, in the same way that if you throw something out of the window of a moving car, it has the car's velocity + the velocity you threw it at. This, however, multiplies it by 0.25 so it's not annoyingly dramatic. Here we use the ApplyForcePolar action definition we just made, to push the projectile with a force of 0.4 in the direction the spell was cast. Then I have it tell the caster to attack the projectile, this is simply to show its attack animation, since the damage it does is negligible to the projectile who has 1000 hp and some regeneration. After the animation finishes, it tells the caster to stop if it's still attacking, so it doesn't try to follow it.
I'd also like to make a function that measures the length of a vector (also using the distance formula), particularly to use on velocity vectors. This will simplify some of our expressions later. It works on 3D vectors, but if you only care about a 2D vector just use 0 for the third parameter. In fact it's practically the same as the Distance3D function, but takes different parameters. If I wanted to I could have just made Distance3D call VectorMagnitude, but too late for that now.
Now we want to put the friction value to use. Obviously it should only affect units that are touching (or close enough) to the ground. The reason for checking the magnitude is since, in my experience with Magecraft, the numbers would eventually get stuck at some really small amount and never reach zero. We want the object to eventually stop moving. Otherwise, the friction coefficient is just that; all velocity vector components are multiplied by it. If you want to add air resistance, go ahead and do that in the else clause for when the unit's height is larger than 0.1. But I'll just ignore that.
Now we just need some gravity. It's an acceleration effect, so it should modify the vertical velocity every game loop. It will only affect units that are not at rest on the ground (height > 0).
Now test it out! It should bounce a few times, slowing down slightly and bouncing lower each time, until it stops. Since this is a football game, I want to make the player unit "lunge" forward every step using this system. That way I can make tackling based on velocity. So first, I set the "Movement - Speed" field of the player unit to 0.25 and the same with the "Animation - Walk Animation Movement Speed" of the unit's actor so it doesn't look funny. Now, I have two player units placed on the map; one for player 1 and one for player 2. But before I can make those units lunge, I need to set their friction and mass and add them to the proper unit group. Remember, custom value 3 is friction and 4 is mass.
Now, to make them actually lunge every half a second, we need to check a few things first. Do it only when the unit has an order, and that order is "move", and the distance to the target is less than some threshold. Then, apply a force in that direction.
I get trigger errors when I tell the unit to do something else like Hold Position, so instead of making that a special case I'm just going to remove it from the unit's command card (among other things).
Now test it out and tweak the numbers until you're satisfied. After I've found a reasonable setup, I had to change the threshold for velocity to stop because it was interfering with my lunges. You might have to as well.
Alright, take a break and make sure to save your map. Now let's allow the players to 'bump' into each other using physics!
Step 4. Collision
First let's add a function called DotProduct. As you may have guessed, it will be used to calculate the vector dot product as described in Step 2, which will simplify expressions later.
Now for the collision equation. Recall from Step 2 that the formula goes:
I = (1+e)*N*(Vr • N) / (1/Ma + 1/Mb)
Va – = I * 1/Ma
Vb + = I * 1/Mb
So that's exactly what I'm doing here, but with elasticity preset to 0.75 so that you see 1 + 0.75 = 1.75 in the expression. If you want you could have that vary depending on the objects colliding.
Edit: there is an error in the above procedure. The Normal vector (normalX, normalY) needs to be a unit vector (length of 1). Achieve this by computing the magnitude by calling VectorMagnitude(normalX, normalY). Store that into a local variable to avoid repeat calculation. Then, before all other actions, divide normalX and normalY by that magnitude:
local mag = VectorMagnitude(normalX, normalY)
normalX = normalX / mag
normalY = normalY / mag
A closer look at the next action:
As you can see this is only made for 2D vectors, which suits my purpose. To make it 3D all you'd have to do is change DotProduct to (X1*X2) + (Y1*Y2) + (Z1*Z2) and add the equivalent Z variables to CollideBounce.
Now simply call it from the Collision action definition. I had to get rid of the units are enemies condition because my test units were allies.
Go ahead and test it, and try walking your unit into player 2's unit. It should slightly push him out of the way. It also helps if your player unit has no collision in the data editor, since we want to do it all manually here (Movement - Separation Radius = 0). The amount pushed may seem underwhelming, that's because the units don't have very much velocity in the first place. So I want to make a new ability called Tackle that causes the player unit to lunge forward much more strongly than by just walking. To create this, all I had to do was duplicate the Throw Ball ability, with the button checked for duplication (but not the effect), changed the button's icon/description/hotkey, give that ability to the player unit and put it on his command card. Review Step 1 if you have trouble with this.
Now, basically all Tackle does is apply a force to the caster. I also added some special effects for extra coolness (a blue explosion on the caster, followed by a trail of smaller blue explosions under the caster as he slides).
Now test it and try tackling the other unit. You may notice it seems a little clunky sometimes; on some occasions the push seems too weak or they simply go through each other. That's because the units are already too far into each other when the collision is checked. What we need to do to fix this is make the units gradually back out of each others' radius until they're just barely not colliding anymore, and then run the collision formula. Start by modifying CollideBounce to call a new action definition named SeparateUnits, and then update the normal vector, before doing anything else.
It's important that SeparateUnits does not create a new thread of execution (in the options). The variable Loops says how many times to run the loop, and it needs to be bigger for larger velocities to fill in the cracks, so it's just 20 * maximum (Unit1's speed, Unit2's speed). This doesn't just move the units away from each other, but rather it moves them in the direction opposite of their velocity so they're sort of going back in time, but it's instantaneous so it's not visible. This will give us a much more true, detailed collision, but at the cost of some more computation. But that computation is only done during collisions, so if collisions are rare it shouldn't be a problem. If it is a problem, try reducing the coefficient on the Loops variable to something less than 20 for a quicker, but less detailed resolution.
You may have noticed in testing that this new collision resolution makes the units seem to pause for a split second while colliding. This is because it's happening right after the unit was moved as part of its normal routine due to velocity, and that movement hasn't had a change to interpolate by blending yet. You can fix this by changing MoveObjects to check for collision before moving the unit. But hey, maybe you like the Street Fighter effect and want to keep it. It's your choice. Also, another little bug I noticed was the unit bouncing up and down slightly even while flat on the ground. The condition in the second red box should fix it.
Test it out. Beautiful, isn't it? And you can do the same thing with projectiles by simply adding CollideBounce to the other cases in Collision. But I won't, because it's not really necessary for a football game.
Step 5. Spell Making
If you've done everything up to this point, you now have the tools to make some truly incredible spells that integrate flawlessly with the physics. Not to mention they're much easier to make. Let's start by touching up the old Throw Ball ability.
What I want to do is make it so the ball is thrown at a varying angle such that it always lands exactly where you clicked on the ground. First I tweaked the strength of gravity (to 0.04 from 0.06) in the Move Objects trigger. Now, since I want there to be some kind of limit to how far you can throw, I just created a variable for it, so that later on we might have that change depending on the unit's "throwing" stat. But for now I'm just going to set it to 30. Also, I removed the part that set the ball's height to 0.75 since starting at anything other than 0 messes up the calculation. Now, the Z velocity of the ball is decided by the Distance from the caster to the target point * some constant. But in order to constrain that distance, I used the Minimum function, with the distance and MaxThrowRange as arguments. Figuring out the constant that makes it land on spot was essentially guess and check, which turned out to be 1/31 for me.
Those spells are pretty basic, but you can do a lot of things just making clever use of the ApplyForce procedures. Now, I'd like to take a look at another map where I've used physics to great success, called Magecraft. In here, things work a bit differently. The physics is only 2D, so the custom values have meaning as follows:
Custom value 0: X velocity
Custom value 1: Y velocity
Custom value 2: Friction coefficient
Custom value 3: Damage coefficient
Custom value 4: Knockback coefficient
Custom value 5: Mass
Custom value 8: Previous terrain height (for detecting cliff collisions)
Custom value 9: Projectile's splash radius
If you don't understand how the spells work in Magecraft, you should play it to get a feel for it, and that might help you in this next part.
First, let's look at the most basic spell: Fireball. As you can see its creation is very similar to the Throw Ball trigger. It uses some variables to modify the damage/knockback based on the spell's level and the player's total damage dealt. The MULE timed life action gives it a 3 second time limit before it dies. For that to work, however, it needs to be given that behavior in the data editor. Also, the fireball's friction is greater than 1.0 which means it actually accelerates.
The following spell is called Scorching Whips. It launches a series of small fireballs in two whip-like patterns starting from behind you and swinging around until they meet in your front.
This is the action definition for Gravity Flux. It has an execution trigger similar to those seen before, but this action definition controls its behavior as it gets called every game loop. Gravity Flux is a slow-moving invulnerable projectile that exerts a pulling force on all player units based on their distance squared, pulling them into its center and dealing damage to those inside it.
Soul Link, one of my favorite spells, is much longer due to its multiple cases. So I'll just post the case where it latches onto a projectile and swings it around you, then throws it back at the enemy who created it. It demonstrates how if you want to apply a force ignoring the mass, just multiply the value by the mass. Basically all this does is make the projectile stop moving, then push it to a random side, and wait for the pull force of the main spell to swing it back around enough that the grappled projectile is moving in the direction of its target (the projectile's caster) then it lets go by killing the soul link.
Baneling Bomb works similarly to Throw Ball, in that it has a trajectory through the air, but then it explodes when it hits the ground. The damage and knockback from the explosion are dependent on the victim's distance from the center of the explosion, such that if a unit is hit directly on point it receives full damage but if it's on the edge of the explosion radius, the effect is negligible. The push is also away from the center of the explosion.
Glaive is a spell that works like a boomerang that you throw out and it returns to you. The trigger is too long and complicated to be worth posting, so I'll just describe it. It has a set destination target, initially that's the target location of the spell. After it reaches that target, it returns to its caster by updating the target point to be the position of the caster each iteration. In order to seek that target, it uses a series of angle comparisons to decide whether it needs to turn left or right. As such, it works fundamentally different than the Homing Missile spell because the force applied to it is always perpendicular to its velocity (which results in slight increases in speed over time, offset by its friction). It repeatedly applies a force to the left or right until it's traveling the correct direction.
Now on the subject of Homing Missile, all it does is apply a constant force in the direction of its chosen target. When cast on a specific unit, it keeps that unit as its target forever. When cast on the ground, it chooses the nearest enemy as its target.
Superball is a newer spell that works sort of like regular projectiles, but instead of dying on impact it bounces to the nearest enemy (or the original caster, but hitting the caster doesn't hurt or push him back, it simply allows him to keep it bouncing around by clever positioning). But not just any nearest unit, however, because the superball shouldn't try to go through the victim it just bounced off! So it needs to use the DotProduct function to determine if the victim is in the way of the candidate target. If it is, then eliminate that candidate. Remember, the dot product returns >0 when the two vectors are facing in the same direction, 0 when they're perpendicular, and <0 otherwise. So it just uses the vector from the victim to the superball and the vector from the superball to the candidate as arguments in the dot product. Now, the only target candidates that can be accepted are those in a 180 degree arc from the superball facing away from the victim.
As you can see, this system is designed for use on a flat ground surface, but I think that's good enough for most purposes. It still works with varying elevation, but since the unit's height is based on the elevation of the ground under it, throwing the football up a cliff will cause its trajectory to jerk up a bit, but it'll still land on the target point. If you encounter any problems following my instructions, let me know. You can contact me in game as LosTacos.877 at North America or in the channel Clan TTG. Thanks for reading!
- To post a comment, please login or register a new account.