| 04-28-2006, 09:07 AM | #1 |
Prerequisites This tutorial is built upon my previous tutorial "Moving Stuff With JASS 1 - Trigonometry (The necessary evil)". If you have not read and understood the original, or have prior knowledge of how trig helps with plotting, this tutorial won't make much sense. You will also need to know, and have implemented, one or another system that allows you to attach variables to handles using a game cache. I've listed a couple of popular ones here:
Introduction In the previous tutorial, we learned how to move stuff from one place to another using trigonometry to determine the new positioning. In this tutorial, we will be using simple animation techniques, incrementing the movement in small chunks so that we can actually see it moving, instead of disappearing from one place and appearing in another. For this, we'll be adding physics formulae to our list. Physics I love physics. Strange since I don't much like trigonometry. Got top of the class in 6th form (dunno international equivalent, but is 2 years before University/College), and if my Dean hadn't asked me to leave school the next year, it would have been the deciding factor in my carrer path. As it is, even 18 years later, I still remember 2 of the formulae for movement - the very two we will be using in this tutorial. Formulae for linear movement. v = d / t v = u + at Key: a = acceleration d = distance travelled t = time elapsed u = original velocity (speed) v = velocity (speed) Although in some cases we might move units in an arc, we can still use linear movement for this. All we have to do is pretend (for the purposes of our calculations) that the arc IS a straight line. Attached Handle Variables I make use of these allot. As I use Vexorian's Caster System, I use functions from his CS_Cache module to achieve this. Whenever you see whe words "Attach" or "Attached" as part of a function name in the following code, it has to do with attaching a variable, retrieving an attached variable or removing attachments. You will need to replace these with the equivalent functions for the system you use. Positioning Helper Function The function "knutz_NewPos" is used throughout the rest of this tutorial. It is used to re-position stuff given the point of origin, distance from point of origin to destination, angle, and boolean for checking pathability. For the code, an explanation of how it works and other required functions for its use, please refer to the tutorial "Moving Stuff With JASS 1 - Trigonometry (The necessary evil)". Configuration Functions These are a big help to any trigger as they allow you to adjust how the functions operate without sifting through the code. For a better explanation of these visit the JESP Spells forum and read the JESP Standard Manifest. It's also a good idea to download a spell or 2 and check out the configuration section of the spell triggers. What we're going to use these for is to return pre-determined values to our main functions. To make them unique (so the function names aren't repeats of others in your map script), replace <SomeTrigger> with the name of your trigger. You will need these to be at the top of your trigger script - above all functions that will call upon them. (Also gives you a handy place to look when you need to change stuff) I'm not sure how distance is measured in WC3, but I assume it's dots or pixels. This is unimportant, except to explain that speed in WC3 is the number of dots/pixels/whatever that an object travels in 1 second, and is a "real" value. Here's the configuration function: JASS:constant function <SomeTrigger>_Speed takes nothing returns real return 1000.00 // dots per second endfunction It's that simple! So whenever we need to refer to the speed within a function we just call <SomeTrigger>_Speed(), and it will return whatever real value we set it to. Distance will be the number of dots we are going to move the stuff. JASS:constant function <SomeTrigger>_Distance takes nothing returns real return 500.00 // dots moved endfunction Acceleration is optional. I don't use it normally, but might come in handy to make a charge spell realistic. JASS:constant function <SomeTrigger>_Acceleration takes nothing returns real return 10.00 // Acceleration of Stuff moved. endfunction If you use acceleration there's something else you need to determine. When the stuff you're moving gets up to speed, will it keep accelerating, or is that speed the fastest it will go? JASS:constant function <SomeTrigger>_TopSpeed takes nothing returns boolean return true // Is <SomeTrigger>_Speed() the top speed? endfunction Film makers talk about "frames per second". The higher number of frames per second, the better the quality of animation. What we're doing here is animating movement, splitting movement into small chunks over time. To achieve this we need to determine the time frame between chunks (how often we take a "snap shot" of where our stuff is now). I use 0.04 (25 "snap shots" per second) as this seems to be the largest number of movements per second you can have without causing lag in the game. (I didn't test this myself, but found this out from the read-me files in Vexorian's Caster System, his CS_Cycle() function returns 0.04) JASS:constant function <SomeTrigger>_SnapShot takes nothing returns real return 0.04 // Time increment endfunction That's all the configuration functions we'll be using in this tutorial. You may want more for your trigger, make as many as you feel is necessary. Animating the Move The principles of animating the move work the same whether you are using distance or angle. I'll de using distance, and I'll be basing this on moving in a straight line, the main functions derived from th straight line movement section of my previous tutorial. Using a Single Speed This is when you've decided to start and finish the move in one speed. Setting Up The Move We will be using timers to control the move as TriggerSleepAction and PolledWait don't seem to be able to handle the small time increments involved. In order for this to work, all our triggeraction function will be doing is creating the timer and attaching to it the variables needed for later functions. Let's get what we need: JASS:function Trig_<SomeTrigger>_Actions takes nothing returns nothing local timer t = CreateTimer() local unit caster = GetTriggerUnit() // Assumes that the trigger unit is the caster of the spell local unit target = GetSpellTargetUnit() // Assumes that the spell targets a single unit local location locC = GetUnitLoc(caster) local location locT = GetUnitLoc(target) Now for the angle of the move: JASS:local real A = AngleBetweenPoints(locC,locT) // Angle between caster and target That's all we need to know for this function. Now we have to ensure that we can access these variables in the movement function by attaching the variables to the timer. For this I use Vexorian's CS_Cache Module (Part of the Caster System), so replace my function calls with whatever you use for this. JASS:call AttachObject(t,"caster",caster) // Both for a point of reference, and also to assign ownership for damage done if we need it. call AttachObject(t,"target",target) // Since this is what we're moving call AttachReal(t,"A",A) // Angle of move Now to access the movement function which we'll call <SomeTrigger>_Move: JASS:call TimerStart(t,0.01,false.function <SomeTrigger>_Move) I use 0.01 for the timer 'cause as far as I know, it's the shortest time it can handle. Please correct me if I'm wrong. Now to clean up the memory leaks: JASS:set t = null set caster = null set target = null call RemoveLocation(locC) set locC = null call RemoveLocation(locT) set locT = null endfunction Let's see that all together: JASS:function Trig_<SomeTrigger>_Actions takes nothing returns nothing local timer t = CreateTimer() local unit caster = GetTriggerUnit() // Assumes that the trigger unit is the caster of the spell local unit target = GetSpellTargetUnit() // Assumes that the spell targets a single unit local location locC = GetUnitLoc(caster) local location locT = GetUnitLoc(target) local real A = AngleBetweenPoints(locC,locT) // Angle between caster and target call AttachObject(t,"caster",caster) // Both for a point of reference, and also to assign ownership for damage done if we need it. call AttachObject(t,"target",target) // Since this is what we're moving call AttachReal(t,"A",A) // Angle of move call TimerStart(t,0.01,false,function <SomeTrigger>_Move) set t = null set caster = null set target = null call RemoveLocation(locC) set locC = null call RemoveLocation(locT) set locT = null endfunction Making The Move Now we need to move our stuff bit by bit, so we can actually see the movement. This function needs to be placed ABOVE Trig_<SomeTrigger>_Actions, but BELOW the configuration functions. JASS:function <SomeTrigger>_Move takes nothing returns nothing Only a function that takes nothing can be used by a timer. This makes it hard to access local variables from a previous function. It is lucky, however, that we saved the needed variables to the game cache, attached to the timer. This is lucky, because we can access the timer. JASS:local timer t = GetExpiredTimer() This will return the timer that triggered this function. Through this, we can access the variables we need. JASS:local unit caster = GetAttachedUnit(t,"caster") local unit target = GetAttachedUnit(t,"target") local location locT = GetUnitLoc(target) local location locT2 = null // Using this for next position of target local real A = GetAttachedReal(t,"A") Now we have the needed variables from the last function, it's time to find out the rest of what we need for the move. First off, the total distance to move. JASS:local real Distance = <SomeTrigger>_Distance() // Our config function Next, how far the target will move between snapshots. v = d / t, so d = v*t. JASS:local real DistInc = <SomeTrigger>_Speed()*<SomeTrigger>_SnapShot() We also need to keep track of how far the target has travelled because.... JASS:local real DistDone = GetAttachedReal(t,"DistDone") // Will return 0.00 first time through We need to check it against the total distance to decide if we're done or not. JASS:if (DistDone < Distance) then So if we're not done, we need to increment DistDone to reflect the next "SnapShot" JASS:call AttachReal(t,"DistDone",DistDone+DistInc) Now we find the snap-shot destination, checking for pathability as we go. JASS:set locT2 = knutz_NewPos(locT,DistInc,A,true) If the way is clear, it's time to move the target. JASS:if (locT2 != null) then call SetUnitPositionLoc(target,locT2) endif Now we can do it all over again by re-starting the timer, using our SnapShot time. JASS:call TimerStart(t,<SomeTrigger>_SnapShot(),false,function <SomeTrigger>_Move) But if we've finished the total move, we need to get rid of the timer, and flush the attached variables from the game cache. JASS:else call CleanAttachedVars_NoSets(t) // Flushes variables attached to timer from game cache call PauseTimer(t) call DestroyTimer(t) // We won't be needing it again endif Now for the usual clean-up. JASS:set t = null set caster = null set target = null call RemoveLocation(locT) set locT = null call RemoveLocation(locT2) set locT2 = null endfunction So let's see that all together. JASS:function <SomeTrigger>_Move takes nothing returns nothing local timer t = GetExpiredTimer() local unit caster = GetAttachedUnit(t,"caster") local unit target = GetAttachedUnit(t,"target") local location locT = GetUnitLoc(target) local location locT2 = null // Using this for next position of target local real A = GetAttachedReal(t,"A") local real Distance = <SomeTrigger>_Distance() // Our config function local real DistInc = <SomeTrigger>_Speed()*<SomeTrigger>_SnapShot() local real DistDone = GetAttachedReal(t,"DistDone") // Will return 0.00 first time through if (DistDone < Distance) then call AttachReal(t,"DistDone",DistDone+DistInc) set locT2 = knutz_NewPos(locT,DistInc,A,true) if (locT2 != null) then call SetUnitPositionLoc(target,locT2) endif call TimerStart(t,<SomeTrigger>_SnapShot(),false,function <SomeTrigger>_Move) else call CleanAttachedVars_NoSets(t) // Flushes variables attached to timer from game cache call PauseTimer(t) call DestroyTimer(t) // We won't be needing it again endif set t = null set caster = null set target = null call RemoveLocation(locT) set locT = null call RemoveLocation(locT2) set locT2 = null endfunction Using Acceleration This is where our starting speed and our final speed are not the same. We'll try a different scenario here. Let's base this on the caster going for a charge. Note: Acceleration doesn't make much difference to the movement, but I'd thought I'd tell you about it anyway. Maybe I just haven't found the right rate. Setting Up The Move JASS:function Trig_<SomeTrigger>_Actions takes nothing returns nothing local timer t = CreateTimer() local unit caster = GetTriggerUnit() // Assumes that the trigger unit is the caster of the spell. Now for the angle. JASS:local real A = GetUnitFacing(caster) // Angle the caster is facing. Save the needed variables to the game cache and attach them to the timer. JASS:call AttachObject(t,"caster",caster) // Since we're moving the guy. call AttachReal(t,"A",A) // Angle of move Start the timer and set it to trigger the move. JASS:call TimerStart(t,0.01,false,function <SomeTrigger>_Move) Now for the usual clean-up JASS:set t = null set caster = null endfunction All together: JASS:function Trig_<SomeTrigger>_Actions takes nothing returns nothing local timer t = CreateTimer() local unit caster = GetTriggerUnit() // Assumes that the trigger unit is the caster of the spell. local real A = GetUnitFacing(caster) // Angle the caster is facing. local real u = GetUnitMoveSpeed(caster) // The "u" part of v=u+at call AttachObject(t,"caster",caster) // Since we're moving the guy. call AttachReal(t,"A",A) // Angle of move call AttachReal(t,"u",u) call TimerStart(t,0.01,false,function <SomeTrigger>_Move) set t = null set caster = null endfunction Making The Move This won't look that great for a small distance, so it's a good idea to change the return value of <SomeTrigger>_Distance at this point. Also, remember to place this function ABOVE Trig_<SomeTrigger>_Actions and BELOW the configuration functions. First thing to do is access the timer, as we need it to get the rest of our required info. JASS:function <SomeTrigger>_Move takes nothing returns nothing local timer t = GetExpiredTimer() Now to retrieve data from the game cache, and set up an empty location to use later. JASS:local unit caster = GetAttachedUnit(t,"caster") local location locC = GetUnitLoc(caster) local location locC2 = null // Using this for next position of the caster local real A = GetAttachedReal(t,"A") // Angle of move. Now to set up our distance variables to keep track of:
JASS:local real Distance = <SomeTrigger>_Distance() // Our config function
JASS:local real DistInc = GetAttachedReal(t,"DistInc") // This will be changing for every snap-shot
JASS:local real DistDone = GetAttachedReal(t,"DistDone") // Will return 0.00 first time through
JASS:local real u = GetAttachedReal(t,"u") OK, so now it's time to work a few things out. First we need to find our new speed using the formula v = u + at. JASS:local real v = u+(<SomeTrigger>_Acceleration()*<SomeTrigger>_SnapShot()) Now we need to check if we wanted to set a top speed, and if we've gone over it. JASS:if ((<SomeTrigger>_TopSpeed() == true) and (v > <SomeTrigger>_Speed())) then set v = <SomeTrigger>_Speed() endif Now to use "v = d / t" to work out our new distance increment, and attach it to the timer for the next round. JASS:set DistInc = v*<SomeTrigger>_SnapShot() call AttachReal(t,"u",v) We need to check it against the total distance to decide if we're done or not. JASS:if (DistDone < Distance) then So if we're not done, we need to increment DistDone to reflect the next "SnapShot" JASS:call AttachReal(t,"DistDone",DistDone+DistInc) Now we find the snap-shot destination, checking for pathability as we go. JASS:set locC2 = knutz_NewPos(locC,DistInc,A,true) If the way is clear, it's time to move the caster. JASS:if (locC2 != null) then call SetUnitPositionLoc(caster,locC2) endif Now we can do it all over again by re-starting the timer, using our SnapShot time. JASS:call TimerStart(t,<SomeTrigger>_SnapShot(),false,function <SomeTrigger>_Move) But if we've finished the total move, we need to get rid of the timer, and flush the attached variables from the game cache. JASS:else call CleanAttachedVars_NoSets(t) // Flushes variables attached to timer from game cache call PauseTimer(t) call DestroyTimer(t) // We won't be needing it again endif Now for the usual clean-up. JASS:set t = null set caster = null call RemoveLocation(locC) set locC = null call RemoveLocation(locC2) set locC = null endfunction So let's see that all together. JASS:function <SomeTrigger>_Move takes nothing returns nothing local timer t = GetExpiredTimer() local unit caster = GetAttachedUnit(t,"caster") local location locC = GetUnitLoc(caster) local location locC2 = null // Using this for next position of the caster local real A = GetAttachedReal(t,"A") // Angle of move. local real Distance = <SomeTrigger>_Distance() // Our config function local real DistInc = GetAttachedReal(t,"DistInc") // This will be changing for every snap-shot local real DistDone = GetAttachedReal(t,"DistDone") // Will return 0.00 first time through local real u = GetAttachedReal(t,"u") local real v = u+<SomeTrigger>_Acceleration()*<SomeTrigger>_SnapShot() if ((<SomeTrigger>_TopSpeed() == true) and (v > <SomeTrigger>_Speed())) then set v = <SomeTrigger>_Speed() endif set DistInc = v*<SomeTrigger>_SnapShot() call AttachReal(t,"u",v) if (DistDone < Distance) then call AttachReal(t,"DistDone",DistDone+DistInc) set locC2 = knutz_NewPos(locC,DistInc,A,true) if (locC2 != null) then call SetUnitPositionLoc(caster,locC2) endif call TimerStart(t,<SomeTrigger>_SnapShot(),false,function <SomeTrigger>_Move) else call CleanAttachedVars_NoSets(t) // Flushes variables attached to timer from game cache call PauseTimer(t) call DestroyTimer(t) // We won't be needing it again endif set t = null set caster = null call RemoveLocation(locC) set locC = null call RemoveLocation(locC2) set locC2 = null endfunction Arcs - Moving Along a Curve By Distance If you're moving something along an arc by distance, it's a simple matter to adapt the above functions to use the formulae from "Moving Stuff With JASS 1", so I won't go into it here. By Angle Easy to adapt for a single speed, just multiply the total angle by the snap_shot time and leave the distance alone - but I just don't know how to add accurate acceleration to it. Smarter people than me can work it out. Author's Notes If you do need any extra examples, just post to this thread and I'll put it on my list. I'm swamped with spell ideas, and I'll be using this tutorial more than anyone, so it's a good bet I'll need some more examples for me anyway. Coming Tutorials:
|
| 04-28-2006, 01:45 PM | #2 |
good for begginers, but I fell most of the JASSers hear are familiar with this, never the less, well done also, I am almost positive timers can go an aweful lots faster then .01, atleast im pretty sure I have done it before, I have used .001 |
| 04-28-2006, 10:44 PM | #3 | |
Quote:
Thanks I need to make it as instant as poss. And yeah, I'd assume most of the JASSers would be. I'm aiming this at those who want to take the step from GUI to JASS, since when I did it about 4 weeks ago, couldn't find any help without bugging people with thousands of questions that were mostly ignored. |
| 04-29-2006, 11:06 AM | #4 |
I suggest you to make your configuration functions that just returns values constant functions instead. Just add a "constant " in front of the "function" in the start of the function block. Anyways, good tutorial, approved :). |
| 04-29-2006, 11:44 AM | #5 |
Great job so far! I particularly like your consistent and appropriate use of locations. A couple suggestions: - Remove set t = null from timer callback. Instead use a function wrapper to fix the leak. - non constant acceleration. one particularly useful formula for modelling motor based systems is acceleration = c * (topspeed - currentspeed) Moving in a circle is pretty much the same as moving in a line. Instead of x,v,a we have theta,omega,alpha. To relate the two sets we have: PolarProjection and AngleBetweenPoints for x and theta Speed of a point on the circumference of a circle: v = omega*r Direction is (x_hat,y_hat) = (-sin(theta),cos(theta)) If you just want to do constant acceleration, a = alpha*r. Within the set, theta,omega,alpha are directly analogous to x,v,a and can be word for word replaced in your examples. |
| 04-30-2006, 08:19 PM | #6 | |
Quote:
I've done this, but I'm not sure why. What's the difference? |
| 05-01-2006, 12:32 AM | #7 |
Wow this will be useful... would it be possible to use the y as flying height, and somehow turn it into a jump? |
| 05-01-2006, 01:29 AM | #8 | |
Quote:
Not sure how that will work, I will think on it. Either way, there's a Jump template in the JESP Spell section by emjlr3 you might want to check out Jump Template |
| 05-06-2006, 09:48 PM | #9 |
Use a timer of 0. A timer of 0 is faster then a timer of 10^-7. This is the smallest modicrom of time in war3. This is slow enough to allow much of the clunk in war3 to happen (dropping item junk, autocast etc), but fast enough that you as a human can't tell it happened. |
| 05-26-2006, 07:50 AM | #10 |
Using a timer of 0. will be lag friendly though, if you had alot of these moving at the same time, serious lag could develop |
| 07-19-2007, 11:01 PM | #11 |
.03 or .04 is the best timer to use so it doesn't lag. Human eye can only see 24 FPS, so that means you shouldn't really move any faster than 30 (.03 means 33 per second, and .04 is 25 per second, slightly above what your eye can pick up) or your just making unecessary stuff for it to compute. And if you have like 20 running at the same time, a .01 or .02 timer can lag it up quite a bit (for crappy computers). |
| 07-20-2007, 03:07 AM | #12 | |
Quote:
Don't just repeat stupid shit, anyone who plays computer games can sure as hell tell the difference between Quake 3 running at 60 FPS and 24 FPS. To me the best balance seems to be 40 FPS (0.0222 timer), of course it'll lag if it's coded poorly. |
| 07-20-2007, 05:22 AM | #13 |
I think that 24FPS number is the low pass knee for your eye, so sensitivity drops off 1/2 or 1/4 per octave above that. |
| 07-20-2007, 08:40 PM | #14 | |
Well thats why you can use .03. If you have a lot of them running at .01 it might lag for poor computers. Quote:
|
| 07-21-2007, 06:34 AM | #15 | |
Quote:
If you do a Google search you'll find tons of forum posts from people repeating the same "24 frames" thing (or a different number), which is supposed to be based off some poorly-conducted scientific study, probably from 20 years ago before computer gaming was popular. The fact that people still quote it like it's a fact is absurd. |
