| 08-16-2011, 04:26 PM | #1 |
Introduction PeriodicLoop is written in Zinc and requires the Jass NewGen Pack along with the latest version of JassHelper.Credits
PeriodicLoop://! zinc library PeriodicLoop requires optional xebasic { /** * The main purpose of PeriodicLoop is to provide the user with a module that implements a fully optimized * periodic loop for a struct. Instances that are added to the loop are stored in a doubly linked list, which improves * the loop's efficiency and allows the removal to be just as easy as the insertion. The user is also provided with * internal wrappers that are meant to be used when dealing with static functions, as opposed to the regular, non-static * methods. These wrappers are implemented with the help of a dummy struct and function pointers. * * Note that this script was written solely for high-frequency loops, and is by default configured to loop 40 times per * second. You can change this interval by editing PERIODIC_LOOP_INTERVAL constant found right below the documentation. * That constant is also public, and as such should be used with all "over time" effects in order to ensure consistency, * accuracy and portability. Neither multiple intervals nor dynamic adjustments in the interval are supported. * * If the xebasic library is present within the map, then the XE_ANIMATION_PERIOD constant will be used instead of * PERIODIC_LOOP_INTERVAL. Please, do not set these constants to anything higher than 0.05, as that would result in lots * of stuff being inaccurate and bumpy. The most commonly used values are 0.025 and 0.03125. * * Now, let's elaborate on the implementation of the script. As I mentioned earlier, you can utilize two "types" of API: * * - For the regular, module-based API, the following things need to be done: * * 1) Implement the "PeriodicLoop" module within your struct. * 2) Declare a non-static method named "onPeriodicLoop". That particular method will be called every interval * as long as there are nodes on the looping list. * 3) When you're willing to add a node, simply call structInstance.startPeriodicLoop(), and when you're willing * to remove it, call structInstance.stopPeriodicLoop(). * 4) Make sure that the module is implemented above all .startPeriodicLoop() calls and below the * "onPeriodicLoop" method in order to avoid unnecessary trigger evaluations. This is very important. * 5) Do not leave destroyed instances in the loop - always call .stopPeriodicLoop() before calling .destroy() or * .deallocate(). * * - Registering static functions is even easier. The API is the following: * * 1) RegisterPeriodicResponse takes response whichResponse returns nothing * 2) UnregisterPeriodicResponse takes response whichResponse returns nothing * * In the above two functions, "response" is a function pointer (you can see its definition below), so "whichResponse" * is the name of a function or a static method that takes and returns nothing. Akin to the "onPeriodicLoop" method, the * passed response will be called every interval until UnregisterPeriodicResponse(response) is called. */ public constant real PERIODIC_LOOP_INTERVAL = 1.0/40.0; type response extends function(); private { trigger mainTrigger = CreateTrigger(); // The trigger onto which the iterators will be attached. timer mainTimer = CreateTimer(); // This timer will be used to periodically evaluate the above trigger. integer that = 0, globalCount = 0; // The former will be used to temporarily store a node, while the } // latter will be used to keep count of the running instances. public module PeriodicLoop { private // Module-private members. { thistype next, previous; // These, along with the null struct, will help us build the doubly linked list. boolean alreadyInserted, pendingRemoval; // Self-explanatory, I hope. static triggercondition instanceIterator = null; // We will use this variable to store the struct's iterator. static boolean threadCrashed = false; // In debug mode, this boolean will help us catch and report thread crashes. } private static method iterator() -> boolean { thistype this = thistype(0).next; // Get the first node. debug { if (thistype.threadCrashed) // Warn the user if the thread had crashed during the last loop. { BJDebugMsg("PeriodicLoop warning: The thread had crashed during the last loop!\n" + "Make sure you are not performing very intensive operations or using uninitialized variables!\n" + "The name of the iterator in which the thread crashed is \"" + thistype.iterator.name + "\""); } thistype.threadCrashed = true; } while (integer(this) != 0) // Traverse the list. { if (this.pendingRemoval) // See if the current node should be removed. { this.pendingRemoval = false; // Remove the node. this.previous.next = this.next; this.next.previous = this.previous; if (this.alreadyInserted) // See if the node should be reinserted. { that = integer(this.next); // Temporarily store the next node. // Reinsert it at the end of the list. thistype(0).previous.next = this; this.previous = thistype(0).previous; thistype(0).previous = this; this.next = thistype(0); if (thistype(that) == thistype(0)) // If the next node was in fact null, this = thistype(0).previous; // then go back to the previous one. else this = thistype(that); // Otherwise, go to the next node normally. } else { globalCount -= 1; // Decrement the global count. if (globalCount == 0) // Check whether the timer should keep running. PauseTimer(mainTimer); if (thistype(0).next == thistype(0)) // Check if the iterator should be detached from the trigger. TriggerRemoveCondition(mainTrigger, thistype.instanceIterator); this = this.next; // Go to the next node. } } else { this.onPeriodicLoop(); // Call the .onPeriodicLoop() method. this = this.next; // Go to the next node. } } debug thistype.threadCrashed = false; // If we made it here, then the thread hasn't crashed. return false; } method startPeriodicLoop() { if (!this.alreadyInserted && integer(this) > 0) // Make sure the method was called on a valid node. { this.alreadyInserted = true; if (!this.pendingRemoval) // First, see if the node was flagged for removal. { // And if it wasn't, insert it at the end of the linked list. thistype(0).previous.next = this; this.previous = thistype(0).previous; thistype(0).previous = this; this.next = thistype(0); globalCount += 1; // Increment the global count. if (globalCount == 1) // Check if the timer should be started (or resumed). static if (LIBRARY_xebasic) TimerStart(mainTimer, XE_ANIMATION_PERIOD, true, static method() { TriggerEvaluate(mainTrigger); }); else TimerStart(mainTimer, PERIODIC_LOOP_INTERVAL, true, static method() { TriggerEvaluate(mainTrigger); }); if (this.previous == thistype(0)) // Check if the iterator should be (re)attached to the trigger. thistype.instanceIterator = TriggerAddCondition(mainTrigger, Condition(static method thistype.iterator)); } } } method stopPeriodicLoop() { if (this.alreadyInserted && integer(this) > 0) // Make sure the method was called on a valid node. { this.pendingRemoval = true; // Flag this node so that it would be removed during the next loop. this.alreadyInserted = false; } } } private struct staticFunctionHelper[] // This is just a dummy struct. { method onPeriodicLoop() { response(integer(this)).evaluate(); } module PeriodicLoop; } public // Below is the API for dealing with static responses. { function RegisterPeriodicResponse(response whichResponse) { staticFunctionHelper(integer(whichResponse)).startPeriodicLoop(); } function UnregisterPeriodicResponse(response whichResponse) { staticFunctionHelper(integer(whichResponse)).stopPeriodicLoop(); } } } //! endzinc Usage examples Example #1:struct periodicLoopExample integer count = 1 real customValue private static method print takes string message returns nothing call DisplayTimedTextToPlayer(GetLocalPlayer(), 0.0, 0.0, 10.0, message) endmethod private method onPeriodicLoop takes nothing returns nothing call .print("Struct #" + I2S(integer(this)) + " has looped for the " + I2S(.count) + ". time!") call .print("customValue = " + R2S(.customValue)) call .print("-------------------------------------------------------") set .count = .count + 1 if (.count > (5 / PERIODIC_LOOP_INTERVAL)) then call .stopPeriodicLoop() call .destroy() endif endmethod implement PeriodicLoop private static method onInit takes nothing returns nothing local thistype this = thistype.create() set .customValue = 3.14159 call .startPeriodicLoop() set this = thistype.create() set .customValue = 2.71828 call .startPeriodicLoop() set this = thistype.create() set .customValue = 1.61803 call this.startPeriodicLoop() endmethod endstruct Example #2:scope periodicLoopExample initializer onInit globals private unit footman = null private integer count = 0 endglobals private function degeneration takes nothing returns nothing call SetWidgetLife(footman, GetWidgetLife(footman) - (20 * PERIODIC_LOOP_INTERVAL)) set count = count + 1 if (count == (15 / PERIODIC_LOOP_INTERVAL)) then call UnregisterPeriodicResponse(degeneration) endif endfunction private function onInit takes nothing returns nothing set footman = CreateUnit(Player(0), 'hfoo', 0.0, 0.0, 0.0) call RegisterPeriodicResponse(degeneration) endfunction endscope |
| 08-16-2011, 08:16 PM | #2 |
There is a problem here. What happens if you call .stopLooping() and then .startLooping() from the onLoop method? The struct instance will be appended at the end of the list while still being the current loop instance, potentially causing a bunch of instances to miss an update. I can think of no simple way to work around this, there is a reason Vexorian used boolean returns instead. |
| 08-16-2011, 10:22 PM | #3 | |
Quote:
Anyway, this approach is quite faster (and arguably more user-friendly) than the one in Vexorian's TimedLoop, so I guess it's all up to you. |
| 08-16-2011, 11:33 PM | #4 | |
Quote:
The simplest way of fixing this I can think of is to flag instances that are stopped and only remove them from the list when you're looping through it. Then you have two options for what to do with instances that are re-started before the loop gets to remove them:
|
| 08-17-2011, 01:21 AM | #5 |
I realize that, but it's still the same instance, so there's still no reason to pause and re-start it... or at least I think so. But anyway, if I were to eliminate that tiny gimmick, the best way would be to simply resort to boolean returns, which simply wouldn't be as nice as it currently is. However, if you mark with such small flaws as unusable, then I guess it can't be helped. Also, I don't know how this module compares to Nestharus'. |
| 08-17-2011, 12:08 PM | #6 | |||
Quote:
I've encountered problems like this when writing my systems before. An earlier version of CineScript which wasn't as robust as the current one would bug out precisely for this reason when I used it in my action map. Quote:
Quote:
|
| 08-18-2011, 12:03 AM | #7 |
Updated. Please let me know if there's anything more to it. And about the issue we've been discussing, I couldn't think of a sane way to make it so that the instance will be inserted at the end of the list (like it normally would be). Instead, I made it work by not removing the instance at all. Truth to be told, I don't know if the small overhead in the loop is even worth it, but I guess that public resources should be as robust as possible. |
| 08-18-2011, 11:40 AM | #8 | ||
Quote:
Zinc:method startLooping() { if (!this.alreadyAdded && integer(this) > 0) // Make sure the method was called on a valid node. { this.alreadyAdded = true; //Set the already added flag first. if (this.pendingRemoval) // First, see if the node was flagged for removal. { // Do nothing here, the periodic loop will handle everything. } else { // And if it wasn't, insert it at the end of the linked list. thistype(0).previous.next = this; this.previous = thistype(0).previous; thistype(0).previous = this; this.next = thistype(0); globalCount += 1; // Increment the global count. if (globalCount == 1) // Check if the timer should be started (or resumed). TimerStart(mainTimer, LOOP_INTERVAL, true, static method() { TriggerEvaluate(mainTrigger); }); if (this.previous == thistype(0)) // Check if this is first node on the list. { // Attach the iterator to the trigger. // Not sure if using an anonymous function here is very conductive to code readability. thistype.instanceIterator = TriggerAddCondition(mainTrigger, static method() -> boolean { thistype this = thistype(0).next; while (integer(this) != 0) // Traverse the list. { if (this.pendingRemoval) // See if the instance should be removed. { // Remove the node from the linked list. this.previous.next = this.next; this.next.previous = this.previous; this.pendingRemoval = false; if (this.alreadyAdded) // See if the instance should be readded. { // Readd it if needed. thistype(0).previous.next = this; this.previous = thistype(0).previous; thistype(0).previous = this; this.next = thistype(0); } else { globalCount -= 1; // Decrement the global count. if (globalCount == 0) // Check whether the timer should keep running. PauseTimer(mainTimer); if (thistype(0).next == thistype(0)) // Check if there are no remaining nodes. TriggerRemoveCondition(mainTrigger, thistype.instanceIterator); // Detach the iterator. } } else { this.onLoop(); // Call the onLoop method that the user is supposed to declare. } this = this.next; // Go to the next node. } return false; }); } } } } method stopLooping() { if (this.alreadyAdded && integer(this) > 0) // Make sure the method was called on a valid node. { this.pendingRemoval = true; // Flag this node so that it would be removed during the next loop. this.alreadyAdded = false; } } Quote:
What counts as significant enough, though? Using trigger conditions makes the biggest difference when you have many structs implementing this module, but each one is only running very few (ideally one) instances. This seems unlikely. In practice, a map is unlikely to have more than a few missile/knockback/etc systems that use this and each of those systems will be running many instances. The only way to get many implementations of this module that each only run a few instances would be through individual triggered spells, but the problem here is that in most maps spells have a lot of downtime when no instances are running. In the end, I don't think the speed gain is significant. However, as long as you add something to detect and report thread crashes in debug mode, I won't insist that you change this. A feature I want to see is the ability to register single functions. I am considering using this in Cinema Workshop to sync all periodic stuff with the camera movement without having to resort to the static if hacks I'm using now. However, I don't want to have to implement this module in a dummy struct just to get my camera system to work. I am reminded of Earth-Fury's KeyAction which provided both static events as well as struct event modules in its API. Both are useful and this system should provide both as well. You could do this easily by implementing your own module in a private struct with some wrappers, something like this: JASS:function interface LoopFunc takes nothing returns nothing private struct events extends array method onLoop takes nothing returns nothing call LoopFunc(this).execute() endmethod implement Loop endstruct function OnLoop takes LoopFunc f returns nothing call events(f).startLooping() endfunction function OffLoop takes LoopFunc f returns nothing call events(f).stopLooping() endfunction endstruct I think the names of methods could be improved. Just "Loop" is insufficient as it ignores the whole periodic aspect of it. Vexorian ended up naming his thing TimedLoop, but his earlier experiments included PeriodicLoop which I think is also a good name you could use. Likewise, .start/stopLooping could be replaced by something like .periodicLoopStart/Stop. I think it would be even better to drop the loop from the name entirely, especially if you add static functions support. The loop is not the point, the point is to run code periodically, the loop is just a way of running the same code for multiple instances. PeriodicEvent works much better as a name for a library like this, although considering you're limited to a single period you'd need a more specific name than one that implies you cover all possible periodic events. Alternatively, though, you could call it PeriodicEvent and add support for different periods. So, in conclusion, these are my two main requirements: static functions support and better names. Although a library specialised in one period is okay (have you considered use xe's period instead of declaring your own, though?), I think supporting various periods is worth looking into since it wouldn't involve any extra cost for users who end up using only one period. |
| 08-18-2011, 03:14 PM | #9 | |||||||
Quote:
Quote:
Quote:
Quote:
Quote:
Quote:
Quote:
|
| 08-21-2011, 12:26 AM | #10 |
Changed the module's name to PeriodicLoop and the methods' names to startPeriodicLoop and stopPeriodicLoop (thanks to Anitarf for the suggestion). Please change the thread's title accordingly. I am still hesitant about including support for static functions. However, if there's anything else that should be either added or improved, please, do let me know. |
| 08-22-2011, 07:18 AM | #11 |
Basing this off of evaluations would eliminate the whole advantage, it'd have to be a module. Personally I'd rather code all this for each script, with just one timer per struct. It avoids a lot of the mess, uses fewer handles and fewer library req's. |
| 08-22-2011, 10:38 AM | #12 | |||||||
Quote:
Zinc:static method() -> boolean { thistype this = thistype(0).next; thistype that // You could use a global to save a local declaration, // globals don't seem to be slower than locals anyway. while (integer(this) != 0) // Traverse the list. { if (this.pendingRemoval) // See if the instance should be removed. { // Remove the node from the linked list. this.previous.next = this.next; this.next.previous = this.previous; this.pendingRemoval = false; if (this.alreadyAdded) // See if the instance should be readded. { // Readd it if needed. set that=this.next thistype(0).previous.next = this; this.previous = thistype(0).previous; thistype(0).previous = this; this.next = thistype(0); set this=that // Go to the next node. } else { globalCount -= 1; // Decrement the global count. if (globalCount == 0) // Check whether the timer should keep running. PauseTimer(mainTimer); if (thistype(0).next == thistype(0)) // Check if there are no remaining nodes. TriggerRemoveCondition(mainTrigger, thistype.instanceIterator); // Detach the iterator. this = this.next; // Go to the next node. } } else { this.onLoop(); // Call the onLoop method that the user is supposed to declare. this = this.next; // Go to the next node. } } return false; }); Quote:
Quote:
The truth is, while as single operations .evaluate and .execute are rather costly, we don't use them at nearly the same frequency as, say, global array reads and writes. Those add up. In practice, doing a periodic update on several instances of a knockback or a missile struct makes shaving 25% off a single .evaluate seem entirely insignificant. Like I already said, that doesn't mean I object to you saving those 25%. It doesn't affect the users in any way, so why not make the system run faster internally? That's fine. But don't spout nonsense about it being a significant gain and definitely don't use such nonsense to justify denying features to the users. If I have a single static function that I want to run periodically, I'd like to be able to do that without having to create a dummy struct to implement your module in so that it can then "loop" through a single static instance. That's just wrong. Besides, it's not even faster. The local variable declaration, a function call with an argument, two array reads, two variable sets and four variable reads (they sure add up, don't they?) likely cost more than 25% of an .evaluate anyway. Edit: I realize some of this overhead would be present even if you did direct evaluations for static functions, since you'd still need to loop through a list of them, but that overhead would be smaller per instance, for example you would only need one local declaration for all instances. Quote:
JASS:private static boolean b=false private method onLoop debug if b then debug // Give an error message warning about a thread crash/op limit hit in the previous loop. debug endif debug set b=true // do the loop debug set b=false endmethod Quote:
Quote:
Quote:
|
| 08-22-2011, 05:46 PM | #13 | ||||
Updated. EDIT: Is the documentation sufficient? Should I provide "usage examples" either within the opening post or embed them into the documentation itself? Quote:
Quote:
Quote:
Quote:
|
| 08-23-2011, 10:20 AM | #14 | |
Quote:
Well there is the option for one timer per struct, or there is the option (using a timer system like this) to use one trigger and one timer for the system, but then a triggercondition and a boolexpr for each struct. It offers a readability advantage sure, and something like this has been approved on the other major communities like HiveWorkshop.com and TheHelper.net. So I don't see a problem that wc3c.net gets their own version. |
| 08-23-2011, 11:51 PM | #15 | |||||
Quote:
As for the documentation, maybe a few things could be reordered a bit, it is very important that the onPeriodicLoop method is declared above the module, so I would mention that in the same place where you first specify that this method must be declared, rather than mentioning this more as an afterthought. The start/stop methods are less important since they're not used as frequently, so I would just mention that using them from above the module is more costly, I wouldn't instruct the users to actually make sure the module is implemented above all such calls, sometimes that's not even possible to do. Likewise, I wouldn't say that static functions shouldn't be used when working with structs, since there can be valid reasons for using them. For example, registering a single static method which then does a ListModule-based loop would be faster than using your module if enough instances are likely to exist simultaneously since this alternative saves a function call per instance while the extra cost of using a full evaluate is paid just once. I guess you meant to say people shouldn't use this to run individual instances but they can't really do that anyway since no arguments can be passed to the function, so how would it know which instance to run? These are just minor quibbles, though, the documentation is fine as it is. Quote:
Quote:
I'm not sure that .evaluate protects the starting thread from a crash, though, since the starting thread is still waiting for a boolean return. It doesn't really matter, though, if static functions can cause a loop crash then the error message will clearly inform the user of that as long as you give your dummy struct a good descriptive name. Also, I'm not sure about the limitations of zinc, but can you use the debug keyword in global declarations as well? You don't need to declare the safety boolean outside debug mode. Quote:
32/s also seems like a rather low frequency to me. I guess speed freaks like to use it so they can then boast about how many instances they can run without lag, but anything below 40/s seems like it would be too choppy to me. Personally, I most often use 50/s for stuff like this. Not that it matters much what the default value is since the constant is configurable, so that's okay. At least you didn't put 32 in the name of the library like some people do. Quote:
|
