| 08-22-2006, 01:06 AM | #1 | |
I've been working on a complete GameCache replacement for attaching variables to objects. I started with SmartTimers and just now extended it to Units, Items as well. I'm writing a description on how it works from the perspective of the user, i.e how to use the system, and I'd like feedback on whether or not it sounds easy to use or complicated. That means YOU. Do YOU understand how to use this based on this post? Does it sound overly complicated or easy and useful? I don't want to get into a technical discussion in this thread. If someone wants to discuss how SmartAttach works, they (or I) can open another thread for that. Why SmartAttach? To give some reasons why you might want to spend your time reading this and give feedback, here is a list of features:
Basic Attach/Get So, this is what it does: You can attach any kind of object to a Timer, a Unit or an Item. The syntax goes like this: JASS://Attaches 1.45 to whichTimer call ItemAttachReal(whichItem, "domain", "key", 1.45) //Attaches whichUnit to whichTimer call TimerAttachUnit(whichTimer, "domain", "key", whichUnit) //Attaches whichEffect to whichUnit call UnitAttachEffect(whichUnit, "domain", "key", whichEffect) //Returns a real. call ItemGetReal(whichItem, "domain", "key") The "domain" value is a bit like MissionKey from gamecache, while "key" is exactly what it looks like. For now, let us assume we always use the value "global" for domain, i.e JASS://Attaches the value 100 to the global key "damage" onto whichUnit. call UnitAttachInt(whichUnit, "global", "damage", 100) The 'whichUnit' variable is not actually of type 'unit', as you might expect, but of type 'integer'. To get the correct kind of integer to pass to Attach/Get functions, use SA_Unit(real_unit), SA_Item(real_item) and SA_Timer(real_timer). JASS:local integer whichUnit = SA_Unit(GetTriggerUnit()) //Returns the integer stored in key "damage" on the Triggering Unit. local integer damage = UnitGetInt(whichUnit, "global", "damage") Note about Timers A special exception goes for Timers: All timers that are later passed to SA_Timer() must be created with SA_TimerNew() or SA_TimerStartNew() instead of the normal CreateTimer() function. Wrong: JASS:local timer t = CreateTimer() //ILLEGAL: t was created with CreateTimer. local integer whichTimer = SA_Timer(t) //Wont work, due to the error above. set location l = TimerAttachLoc(whichTimer, "global", "target", GetUnitLoc(someUnit)) JASS://Creates a timer but returns an Integer. local integer whichTimer = SA_TimerNew() //Attaches a location to the newly created timer under key "target" set location l = TimerAttachLoc(whichTimer, "global", "target", GetUnitLoc(someUnit)) Global and Local namespace In MOST CASES you want to use "global" as the domain. The only exception is if the object you attach to is a special purpose object. For example, if you have a timer that exists ONLY to drive a spell called "Snowball", you can use "snowball" as the domain. JASS:function SnowballSpell takes unit caster, real speed returns nothing //Create and start a new timer that repeats every 0.04 seconds. local integer whichTimer = SA_TimerStartNew(0.04, true, function SnowballSpellCallback) //Attach the caster with key "caster" to the timer using local domain "snowball" call TimerAttachUnit(whichTimer, "snowball", "caster", caster) //Attach the speed call TimerAttachReal(whichTimer, "snowball", "speed", speed) endfunction The callback, SnowballSpellCallback can then look like this: JASS:function SnowballSpellCallback takes nothing returns nothing //This timer was earlier created with SA_TimerStartNew() local integer whichTimer = SA_Timer(GetExpiredTimer()) //Get the unit stored in key "caster" in the local domain "snowball" on the timer. local unit caster = TimerGetUnit(whichTimer, "snowball", "caster") //Get the speed attached to the timer. local real speed = TimerGetReal(whichTimer, "snowball", "speed") //Do some spell-stuff endfunction The local domain is good for two things. First, it guarantees that the keys don't collide with any global keys, which means the following is safe. JASS:call UnitAttachGroup(whichUnit, "global", "g", someGroup) call UnitAttachGroup(whichUnit, "snowball", "g", someOtherGroup) //g1 becomes someGroup set g1 = UnitGetGroup(whichUnit, "global", "g") //g2 becomes someOtherGroup set g2 = UnitGetGroup(whichUnit, "snowball", "g") So the local domain makes it easy to not worry about colliding with global, non-specific keys. The second benefit is that SmartAttach can optimize memory usage, sharing memory between two local domains that cannot exist at the same time. Which brings me to the next point, every object can only have ONE local domain. JASS:local integer whichUnit = SA_Unit(GetTriggerUnit()) //So far so good call UnitAttachItem(whichUnit, "snowball", "fun", someItem) //WRONG: the "snowball" local domain and "other_domain" for the same //object makes both this AND THE PREVIOUS LINE incorrect. call UnitAttachItem(whichUnit, "other_domain", "fun", someItem) //WRONG: It doesn't make a difference if the keys are different. call UnitAttachItem(whichUnit, "other_domain", "other_key", someItem) Other notes: You can NOT use a variable for domain or key. JASS://WRONG: 'some_variable' is not a "literal" string call UnitAttachInt(someUnit, some_variable, "key") //WRONG: still not a literal string call UnitAttachInt(someUnit, "wawa" + I2S(10), "key") //CORRECT: A simple literal string please! call UnitAttachInt(someUnit, "domain", "key") To summarize domains, remember that you can always store and read anything from the "global" domain, and use a local domain only for objects that serve a special purpose, and only ONE local domain for any given object. Cleaning up Finally, all that is created must be destroyed. For every Unit, Item or Timer that is EVER converted to an integer using SA_Unit, SA_Item or SA_Timer, a corresponding SA_UnitRelease, SA_ItemRelease, SA_TimerRelease must be called. The functions take two boolean arguments that should normally be set to true. JASS:local integer whichItem = SA_Item(GetManipulatedItem()) //...Later (may be in another function) call SA_ItemRelease(whichItem, true, true) Now to make one thing clear. You only need to call for example SA_ItemRelease when the item is destroyed. You do NOT have to call it at the end of functions to prevent leaks or anything like that. You can call SA_Item(someItem) a million times for the same item, the only thing you need to remember is to call SA_ItemRelease ONCE (AND ONLY ONCE) when the item leaves the game. The same goes for units and timers. What's with the two boolean parameters?
Once you call SA_ItemRelease(someItem) you do NOT pass the value of someItem to any other function. Syntax Summary This may sound like a whole lot of things, so let's summarize it to see how easy it really is: JASS://A unit local unit u = CreateUnit(parameters) //Get an integer that can be passed to all SA functions. local integer whichUnit = SA_Unit(u) //Attach a value call UnitAttachInt(whichUnit, "global", "your_value", 100) //Retreives the value set your_value UnitGetInt(whichUnit, "global", "your_value") //At some point, i.e unit dies event: //Take the dying unit, convert it to an SA_Unit integer, and release it. call SA_UnitRelease(SA_Unit(GetDyingUnit()), true, true) That's 95% of the functionality right there. How to use with a map? Some advanced JASSers have probably guessed that this system does a lot of peculiar things. To make it all work, you have to
Attached is a beta-version of the SmartAttach compiler and a test map that shows smart attach in action using a large amount of 0.04 timers, storing and retreiving values and moving objects without lag. If you wish to try out SmartAttach, copy the declarations from the test-maps custom script section into your own map before compiling. Note that the compiler is beta, so tell me if it works or not. About the test-map: For every unit you summon, that unit will be "linked" to a RANDOM unit on the map. If that unit dies, so does the link-unit. Summon a lot and watch it to know what I mean :) |
| 08-30-2006, 11:24 AM | #2 |
Personally I don't mind wrappers over constructors and destructors. Afterall it won't take me longer to write SA_TimerNew() instead of CreateTimer(). And the rest is exactly as easy as using gamecache with CSCache. So I really don't see any reason not to use this, as it's much faster than gamecache. The one domain per object rule helps keeping it organized, I don't have a problem with that. I'm not sure what Vex is using for his new CSCache (is it available already?), but of course then we'll have to compare speeds again and see what is the overall better solution. But for now this beats gamecache. ![]() |
| 08-30-2006, 03:13 PM | #3 | |
Quote:
|
| 08-30-2006, 03:47 PM | #4 |
That is rarely a limitation. The vast majority of game cache use is for structure-like use which this compiles down *statically*. For arrays there are several alternative options including one karu is cooking up. It would be neat if use of variables switched it from static compilation to array use but that's also an inconsistency in internal behavior. You can argue both ways about whether or not it should automatically decide whether or not to work in all situations. |
| 08-30-2006, 07:16 PM | #5 |
Before I start this post, I would like to point out one thing: In any case where SmartAttach for some reason does not fit the need of a given problem, GameCache can be used. (GameCache will even be slightly faster than usual, as the normal I2S call is replaced with an inlined array reference). === Yeah, I did toy with the idea of automatically detect whether key is a variable, but in addition to Pipe's argument that it makes for inconsistent behaviour, I also strongly believe that invariable keys are best practice. Attaching "key" to unit covers a specific purpose, namely that of structuring data around an object. When you wish to attach variable data to an object, it is usually because you want to attach multiple values, right? That is why I recently added both SmartStack and SmartArray, which will further extend the cases in which the system can be used. SmartStack is mostly a tool used internally (although it certainly has other uses), but SmartArray should be greatly beneficial for anyone. For example, you can define a smart array named "Snowballs" that holds units and give it a size of max 32 elements with maximum for example 128 such arrays alive at any give time. (Those values are of course configured seperately for every defined SmartArray) JASS:function SnowballSpell takes unit caster, integer num, real damage returns nothing local integer i = 0 local unit u //Get a new "Snowballs" array local integer unit_array = UnitArrayNew("Snowballs") loop exitwhen i == num //Create a snowball unit set u = CreateUnit(...) //Attach the damage to the newly created snowball call UnitAttachReal(SA_Unit(u), "Snowball", "damage", damage) //Attach the caster unit to the snowball so that damage dealt later can come from the right source call UnitAttachUnit(SA_Unit(u), "Snowball", "source", caster) //Add the snowball unit to the array at position 'i' call UnitArraySet("Snowballs", unit_array, i, u) set i = i + 1 endloop //Finally, attach the entire array of snowballs to a new timer that can move them around etc (the array is just an integer remember) call TimerAttachInteger(SA_TimerStartNew(0.04, True, function SnowballMove), "Snowball", "snowball_array", unit_array) endfunction //SnowballMove could look like this function SnowballMove takes nothing returns nothing local integer i = 0 local integer t = SA_Timer(GetExpiredTimer()) local integer unit_array = TimerGetInteger(t, "Snowball", "snowball_array") loop //Do stuff with each snowball endloop endfunction There are still some details I need to figure out to make SmartArrays even more flexible. That's why any feedback like, "with this system I can't do this" or even better "how can I do X with this system?" is very valuable. Thanks for the feedback so far though, I've been very lucky to have some clever people to chat with while implementing this and I am close to a release of the entire toolset which should make using SmartAttach painless. |
| 08-31-2006, 11:11 AM | #6 |
My Buran Lib is lower but more universal http://xgm.ru/forum/attachment.php?attachmentid=8509 PS the code is in common.j =) is more handly for all... |
| 08-31-2006, 01:54 PM | #7 |
This seems nice. I wont use it since I enjoy making systems myself. I've just got a question. JASS:function SA_ly takes location l returns location return GetLocationY(l) return null endfunction JASS:function SA_GetLocationY_Loc takes location loc returns location set bj_enumDestructableCenter=SA_ly(loc) return bj_enumDestructableCenter endfunction |
| 08-31-2006, 02:52 PM | #8 |
Because there's a problem with return-bugging reals, storing them in a variable first fixes it. |
| 08-31-2006, 06:12 PM | #9 |
As a user, I am not satisfied with having to run another program over the map after I save it to make it work. Could you do without that? Also, what's the use of having domains anyway? We could always inline the domain string into the key if we needed to organise stuff, so I see no need for it. It only complicates matters because you can't have more than two of them anyway. |
| 09-01-2006, 08:10 AM | #10 |
@iNfraNe: Yeah, we all love to make our own systems :D Also, I'd like to point out that I don't use location stacks anymore, assuming that's where you saw that code. SmartAttach now uses it's own SmartStack for internal stacks, which is a gazillion times faster :) @Anitarf: I have not really disclosed the details of this system, but to answer your questions of whether an external program is needed, I have to do that. Why a compiler: Basically, the compiler transforms all the above mentioned calls into tiny inlined set/get array calls. This makes a Get/Set functions so fast that, compared to GC Get/Set, it takes 0 time. And yes, I mean virtually 0 time. Now I am aware of people's general resistance to doing something as mind bogglingly time consuming and exhausting as having my GUI program running in the background and press 'Compile' every time they save their map. ;) Luckily, PipeDream's Grimoire let's my soon to be released full compiler automatically parse the map as soon as you press Test Map in the world editor. And as a bonus, you get to use PJASS for syntax checking. So, I really do hope that I do not have to hear any "this is too difficult" arguments, because I really do believe that this system should be used by any advanced mappers needing to attach variables. Why domains: As for the need for domains I'd be glad to explain why it is of prime importance. First, I consider it cleaner to have a seperate 'domain' and 'key' system as opposed to ad-hoc and non-enforced "domain_key" unique keys. It enforces the use of a domain (even if that domain is global, but believe me, 'global' will not be the most used domain in practice), and the enforcement is by itself important. Second, again I have to delve into the implementation, all local domains share memory. Because object A (i.e a timer) by definition 'exists' in only one local domain, the implementation can use this to optimize memory usage. TimerAttachUnit(t, "Snowballs", "caster", u) may very well use the same memory location as TimerAttachUnit(t, "FireSpell", "target", u). This memory sharing is important, because without it a very large and complex maps would end up with an excessive amount of memory usage. As for whatever Toadcop posted, I can only say that if it does not need a compiler to transform, it isn't anything like this system. It is about time I write down a detailed explanation of what this system really does that makes it so much better than GC, because I certainly admit without that knowledge, the need for SmartAttach might be hard to figure out :) Also, here is a few additions I am adding to it today: JASS://Create a unique Id integer that can have values attached to it //Good when you need to attach stuff to just 'anything'. //A complex spell might just create a new id, attach a bunch of data //to it, and then attach this Id to related units, timers etc. set id = SA_IdNew() call IdAttachXXX(id, "domain", "key", value) call IdGetXXX(id, "domain", "key") //Also, to make SmartArrays 100 times more useful, //all kinds of SmartArrays support Push/Pop/Remove/Size //Puts 'u' to the end of the array-as-stack if UnitArraySize("Fireparticles", i) < 32 then call UnitArrayPush("Fireparticles", i, u) endif //Retreives and removes the last unit in the array-as-stack if UnitArraySize("Fireparticles", i) > 0 then set u = UnitArrayPop("Fireparticles", i) endif //Finds and removes the unit u from the array-as-stack call UnitArrayRemove("Fireparticles", i, u) //Loop through all items set x = 0 loop exitwhen x == UnitArraySize("Fireparticles", i) call EatUnitAlive(UnitArrayGet("Fireparticles", i, x)) set x = x + 1 endloop //Note: You either use a SmartArray as a stack or as an array //not both at the same time. Although, looping through it as above //is just fine :) |
| 09-01-2006, 08:49 PM | #11 | |
Quote:
|
| 09-01-2006, 09:43 PM | #12 | |
Quote:
|
| 09-02-2006, 08:21 AM | #13 | |
Quote:
Actually, if you want, you can do exactly that. But I can certainly understand why you wouldn't want that. I wouldn't recommend using SmartAttach just for the sake of using it. How much speed impact does Get/Set values have in your cinematic system? Would it even matter? Unless SmartAttach lets your system do more with less lag, and noticably so, it doesn't really need to use it does it? I can not decide for others where SmartAttach will be of use, but I at least want to make it clear that using the system isn't cumbersome to use. In fact, compared to the work involved in implementing someones system, copying triggers and whatnot, this compiler is a piece of cake. You just run it on a map or let WE run it on your map. Some more complete docs and a first release seem to be coming up this weekend and I hope people will keep their minds open until the real thing is here. Again, thanks for the feedback :) |
| 10-03-2006, 11:09 PM | #14 |
JASS://Create a unique Id integer that can have values attached to it //Good when you need to attach stuff to just 'anything'. //A complex spell might just create a new id, attach a bunch of data //to it, and then attach this Id to related units, timers etc. set id = SA_IdNew() call IdAttachXXX(id, "domain", "key", value) call IdGetXXX(id, "domain", "key") |
| 10-03-2006, 11:58 PM | #15 |
That's a good question. IDs are something that haven't been explained well. Lets say you are making a spell that creates some STUFF and FX and, you know, whatever a spell makes. I would write that using an ID. JASS:function AwesomeSpell takes unit caster, unit target, real damage returns nothing //Create a new id to store all sorts of spell stuff in. local integer spell_id = SA_IdNew() //Create a timer to handle the periodic activity of the spell local integer spell_timer = SA_TimerStartNew(0.05, true, function AwesomeSpellHandle) //Attach the caster of the spell to the id call IdAttachUnit(spell_id, "AwesomeSpell", "caster", caster) //Attach the target of this spell call IdAttachUnit(spell_id, "AwesomeSpell", "target", target) //Attach the damage the spell does call IdAttachReal(spell_id, "AwesomeSpell", "damage", damage) //Attach this id to the timer call TimerAttachInt(spell_timer, "AwesomeSpell", "spell_id", spell_id) //Done, now let the handle function do it's job endfunction //The following function should be ABOVE, but I put it below to make it more readable function AwesomeSpellHandle takes nothing returns nothing //Retreive the id of the spell local integer spell_id = TimgerGetInt(SA_ExpiredTimer(), "AwesomeSpell", "spell_id") //Get the stuff we stored local unit caster = IdGetUnit(spell_id, "AwesomeSpell", "caster") local unit target = IdGetUnit(spell_id, "AwesomeSpell", "target") local real damage = IdGetReal(spell_id, "AwesomeSpell", "damage") //Now we can do our periodic stuff, whatever that may be. endfunction This should make it obvious why IDs are very very powerful. Whatever the spell creates, whether projectiles, special effects, sounds - it can all be stored onto one id, so when you need to cleanup the spell, its all easily stored there. SA_IdNew() just returns a unique integer. Therefore it must be released using SA_IdRelease(whichId) afterwards to assure you don't run out of unique ids. If you look at the Lightning Storm example in the WeWarlock beta thread, you will see IDs being used to their potential by creating a multisegmented lightning (that spell uses up to 100+ ids at the same time) |
