Understanding Strucs-Should I convert this array based hitbox system into structs?

kupo15

Member
I'm about ready to look into strucs and want to theorize a better way to code this existing system I have. It seems like converting it to structs away from arrays might be a good choice but I don't know enough about struc to know for sure.

Each character has their own script of move hitboxes that is one long 2D array for each move they have. Here is a snippet of one move's hitbox attributes (out of 30 moves so each script is 30x longer than what you see here)

GML:
// ATTACK 0: UPPERCUT //
    var b = charAnimation.attack0;
                move[b][0] = b;
                move[b][1] = 0; // if 0 single hit; 1 is multiple active hitboxes
                move[b][2] = 34; // IASA
                move[b][3] = 3; // charge frame           
                move[b][4] = 0; // 0 if melee move, 1 if projectile
               
                // FRAME STARTS
                move[b][10] = 4; // frame hitbox 1 starts
                move[b][11] = noone; // frame hitbox 2 starts
                move[b][12] = noone; // frame hitbox 3 starts
                move[b][13] = noone; // frame hitbox 4 starts
                move[b][14] = noone; // frame hitbox 5 starts
               
                // FRAME ENDS
                move[b][20] = 11; // last frame of hitbox 1
                move[b][21] = noone; // last frame hitbox 2
                move[b][22] = noone; // last frame hitbox 3
                move[b][23] = noone; // last frame hitbox 4
                move[b][24] = noone; // last frame hitbox 5
               
                // DAMAGE
                move[b][30] = 16; // hitbox 1 damage
                move[b][31] = noone; // hitbox 2 damage
                move[b][32] = noone; // hitbox 3 damage
                move[b][33] = noone; // hitbox 4 damage
                move[b][34] = noone; // hitbox 5 damage
               
                // KNOCKBACK STRENGTH
                move[b][40] = 8.1; // hitbox 1 knockback strength
                move[b][41] = noone; // hitbox 2 knockback strength
                move[b][42] = noone; // hitbox 3 knockback strength
                move[b][43] = noone; // hitbox 4 knockback strength
                move[b][44] = noone; // hitbox 5 knockback strength
               
                // KNOCKBACK ANGLE
                move[b][50] = 10; // hitbox 1 knockback angle + 90
                move[b][51] = noone; // hitbox 2 knockback angle + 90
                move[b][52] = noone; // hitbox 3 knockback angle + 90
                move[b][53] = noone; // hitbox 4 knockback angle + 90
                move[b][54] = noone; // hitbox 5 knockback angle + 90
                 move[b][55] = 5; // hitbox 1 angle scaling
                
                // KNOCKBACK STUN TYPE
    /* 0 is grab; 10 is random flinch; 11-19 is specific flinch;
    20/25 is random back/front knockdown; 21-24/26-29 is specific back/front knockdown */
                move[b][60] = 20; // knockback type
                move[b][61] = noone; // knockback type
                move[b][62] = noone; // knockback type
                move[b][63] = noone; // knockback type
                move[b][64] = noone; // knockback type
               
                // ELEMENT SFX
                move[b][70] = noone; // 0 normal element; 1 fire; 2 electric; 3 ice
                move[b][71] = noone; // element
                move[b][72] = noone; // element
                move[b][73] = noone; // element
                move[b][74] = noone; // element
               
                // HIT SFX
                move[b][80] = 5; // hit sfx type
                move[b][81] = noone; // hit sfx type
                move[b][82] = noone; // hit sfx type
                move[b][83] = noone; // hit sfx type
                move[b][84] = noone; // hit sfx type
               
                // WHIFF SFX
                move[b][90] = 3; // whiff sfx type
                move[b][91] = noone; // whiff sfx type
                move[b][92] = noone; // whiff sfx type
                move[b][93] = noone; // whiff sfx type
                move[b][94] = noone; // whiff sfx type
               
                // HURT SFX
                move[b][100] = 2.5; // hurt sfx type
                move[b][101] = noone; // hurt sfx type
                move[b][102] = noone; // hurt sfx type
                move[b][103] = noone; // hurt sfx type
                move[b][104] = noone; // hurt sfx type
               
                // GRUNT SFX
                move[b][110] = noone; // grunt sfx type
                move[b][111] = noone; // grunt sfx type
                move[b][112] = noone; // grunt sfx type
                move[b][113] = noone; // grunt sfx type
                move[b][114] = noone; // grunt sfx type
               
                // SHIELD STUN
                move[b][120] = noone; // shield stun
                move[b][121] = noone; // shield stun
                move[b][122] = noone; // shield stun
                move[b][123] = noone; // shield stun
                move[b][124] = noone; // shield stun
               
                // SHIELD KNOCKBACK
                move[b][130] = noone; // shield knockback
                move[b][131] = noone; // shield knockback
                move[b][132] = noone; // shield knockback
                move[b][133] = noone; // shield knockback
                move[b][134] = noone; // shield knockback
               
                // HIT STUN
                move[b][140] = noone; // hit stun
                move[b][141] = noone; // hit stun
                move[b][142] = noone; // hit stun
                move[b][143] = noone; // hit stun
                move[b][144] = noone; // hit stun
               
                // HITLAG
                move[b][150] = noone; // hitlag
                move[b][151] = noone; // hitlag
                move[b][152] = noone; // hitlag
                move[b][153] = noone; // hitlag
                move[b][154] = noone; // hitlag
               
                // CAMERA SHAKE MULTIPLIER
                move[b][160] = 1; // camera shake
                move[b][161] = 1; // camera shake
                move[b][162] = 1; // camera shake
                move[b][163] = 1; // camera shake
                move[b][164] = 1; // camera shake
Takeaways in understanding this are the following:

1) the (b) array slot is the sprite number for the animation (62 is spr_attack0 so move[62,10] is attack0's hitbox 1 frame start)

2) I allotted for each move to have up to 5 hitbox data (so one move could hit 5 times with different properties or 1 hitbox can change values throughout the animation 5 times for example)

3) index[b,0-9]] are general hitbox attributes (not relevant to this thread, really)

4) Each group of 10 numbers aligns with the same hitbox. ie:
[10] hitbox 1 frame start
[20] hitbox 1 frame end
[30] hitbox 1 damage
[40] hitbox 1 strength
etc....
[11] hitbox 2 frame start
[21] hitbox 2 frame end
[31] hitbox 2 damage
[41] hitbox 2 strength
etc....


The reason I did this with arrays was so I could easily loop through arrays to copy data, also bc the first array is the player's sprite index, I can use that to pull correct hitbox data as needed and link it to the player (ie if move[sprite,10] == w/e). If you have any questions on how other systems use this data to understand why I initially did this to better understand feel free to ask. I didn't want to overload with too much front information. I built this system 6 years ago when none of these strucs were available and I wasn't as good a programmer. Its obvious now this whole system feels like spaghetti code that works somehow. It would be a huge overhaul to change but if it makes sleep better at night knowing its consolidated and solid its something I'm interested in doing haha

TLDR:
Would this be a good candidate to use strucs instead of arrays? If you make suggestions on how to change this into strucs, be prepared for me to tell you the potential problems I see with how it interacts with other systems. I will do this not to push back against you but it seems like the easiest way to give you the fuller picture of how everything connects together

@GMWolf this is what we briefly talked about in one of my recent status updates, obviously the status wasn't the place to go into full depth compared to here haha
 

samspade

Member
Structs basically have two big difference to arrays (if you exclude constructors which I would since you can create a function that would make an array form a template). First, they have named variables that are accessed by the dot accessor. Second, and far more importantly, they have their own scope. Essentially this means you can think of them like objects (hence the dot accessor and ability to use the with statement on them) but without all of the built in stuff that a GM object has. So if it would make sense to replace your data with an object, then I would say yes. Otherwise, I would say no (unless you really want named variables for some reason and even then I would use enums).

I would say it makes sense to replace data with an object if you want data to have functions associated with it. For example, you can do vectors with arrays (data) but it is kind of a pain because you want to be able to do different things to those vectors and if they are arrays you're constantly passing those arrays around and in to functions because it is just data. If the vectors are structs (objects) you can simple tell the vector to do an operation on its own data. You no longer have to pass it around.
 

kupo15

Member
Structs basically have two big difference to arrays (if you exclude constructors which I would since you can create a function that would make an array form a template). First, they have named variables that are accessed by the dot accessor. Second, and far more importantly, they have their own scope. Essentially this means you can think of them like objects (hence the dot accessor and ability to use the with statement on them) but without all of the built in stuff that a GM object has. So if it would make sense to replace your data with an object, then I would say yes. Otherwise, I would say no (unless you really want named variables for some reason and even then I would use enums).

I would say it makes sense to replace data with an object if you want data to have functions associated with it. For example, you can do vectors with arrays (data) but it is kind of a pain because you want to be able to do different things to those vectors and if they are arrays you're constantly passing those arrays around and in to functions because it is just data. If the vectors are structs (objects) you can simple tell the vector to do an operation on its own data. You no longer have to pass it around.
Thanks so much for that explanation! It would seem like structs may not be suitable for this then because creating an object for each hitbox data sounds ludicrous to do. This list is simply data.

It seems like a much better step to instead reorganize how I setup this system in the first place using ds_lists instead because the first thing that occurs when a hitbox object is created, is to reference an array segment (like above using sprite as the determining factor) then create a ds_list inside that object via looping through the move array. What's the point in that when I can just create each hitbox as a ds_list then simply pass on the pointer to that list? It also seems much better to created a nested structure that way I can have no limit to the number of hitboxes a move can contain. In this situation, list entry 10 would be a list of hitboxes which contains a list of attributes for that hitbox.

Seems much cleaner than using structs, don't you think? Structs might only be useful if I wanted any character to access any hitbox but that doesn't make sense at all given that hitboxes are specifically tailored to specific characters and their actual moves.
 
Last edited:

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
It would seem like structs may not be suitable for this then because creating an object for each hitbox data sounds ludicrous to do.
I wouldn't see it as much as creating an object for each hitbox data, rather as creating an object instance for each hitbox data. Kind of like you don't create a new object asset for each inventory item, but instead create a new item definition instead (or maybe you create an object; that also sort of depends on what the inventory means).

Anyway.

When I look at this code, I'm thoroughly disoriented, and it'd be a pretty daunting task when it comes to both defining the new hitbox information and using the hitbox information elsewhere. The fact that lots of array information is empty doesn't help at all.

So, a few steps to make this somewhat more readable:

1. There's something called "charAnimation.attack0". I don't know if it's a number from 0 to 29, or a sprite index, or something else altogether. But what if it was a struct instead? Something like { sprite: spr_Ninja_attack0, moveInfo: ... }? Where hitboxes would be this whole array at first, maybe, until we refactor it further?

2. There's awfully lot of move[b] references. How about making it var moveInfo = move[b]; or - per earlier suggestion - var moveInfo = charAnimation.attack0.moveInfo? Maybe there's a more accurate name for it than moveInfo? That's all the more reason I'd want to rename "move[ b ]" to "moveInfo", even if the former is technically shorter; at least with a descriptive name I have a general idea of what I'm dealing with.

3. Right now, moveInfo is a massive flat array. How about nesting the arrays instead? Since GM:S 2.3+ jagged arrays are perfectly usable (with chained accessors working properly), so you are not limited to grid-like 2D arrays anymore. With this, the moveInfo array can be somewhat simplified to something like:
GML:
moveInfo[0] = b; // is it a sprite or index or what?
moveInfo[1] = 0;      // if 0 single hit; 1 is multiple active hitboxes
moveInfo[2] = 34;     // IASA
moveInfo[3] = 3;      // charge frame           
moveInfo[4] = 0;      // 0 if melee move, 1 if projectile
moveInfo[5] = [4];    // FRAME STARTS; can also be something like [4, 6, 8, 12] if needed, because it's a nested array
moveInfo[6] = [11];   // FRAME ENDS
moveInfo[7] = [16];   // DAMAGE
moveInfo[8] = [8.1];  // KNOCKBACK STRENGHT
// ...and so on
4. Or maybe make moveInfo items straight-up named? Aside from going all struct-based, you can also used arrays with its indices encoded in enums for readability and easier refactoring. The latter is better for performance-critical applications, but if you aren't accessing hitbox data hundreds or thousands times per Step the struct overhead will be probably negligible.
GML:
var moveInfo = {
    sprite: b,  // seriously, though, what is b?
    hitboxType: HitboxType.Single
    iasa: 34,  // whatever IASA is
    chargeFrame: 3,
    moveType: MoveType.Melee,
    frameStarts: [4],
    frameEnds: [11],
    frameDamage: [16],
    frameKnockbackStrenght: [8.1]
};
charAnimation.attack0.moveInfo = moveInfo; // note that in the end any newly created struct must be assigned to the owner
Note that at this point I don't even need comments to know what's going on in here - the names themselves are descriptive enough.

5. Finally, you may want to organise per-frame informations in their own structs, instead of having N+1 parallel arrays with indices corresponding to specific frames:
GML:
var moveInfo = {
    sprite: b,
    hitboxType: HitboxType.Single
    iasa: 34,
    chargeFrame: 3,
    moveType: MoveType.Melee,
    frames: [
        { start: 4, end: 11, damage: 16, knockbackStrenght: 8.1, ... }
    ]
}
I don't know if whatever I wrote even corresponds to your arrays structures, but I'm pretty sure it's more readable. If anything, the fact that I'm not certain I understood the hitbox data correctly is all the more proof it's hard to figure out.

Note that definitions code may also use functions/constructors. E.g. you may want to consider a function like createHitboxFrame(start, end, damage, knockbackStrength, ...) if typing all these out is too much; personally, I'd stick with the code from point 5 for easier at-glance readability, but at this point it becomes pretty much a matter of preference. functions/constructors can be really useful when returned data isn't a struct, but a populated array instead.

Also, remember that structs in general perform slower than arrays, as far as variable lookups are concerned. Again, I don't think you'll need to access hitbox/move data often enough for it to seriously affect the performance (usually I'd figure there are several fighters invovled, rather than several dozen), but it's still worth keeping in mind. If structs overhead is too much, I suppose a classic array-indexed-by-enums is a good balance between readability, usability and performance.
 

kupo15

Member
Really appreciate the in depth response!


When I look at this code, I'm thoroughly disoriented, and it'd be a pretty daunting task when it comes to both defining the new hitbox information and using the hitbox information elsewhere. The fact that lots of array information is empty doesn't help at all.
Oh most definitely! Its so poorly written and designed even though its relatively easy once you understand it, but its still really too complicated than it needs to be hard to search through all that code to find the section you want because there are 30x the amount of code as in that snippet to search through because there is 30 attack slots/animations :O The blanks were there for placeholders so I knew where to stop, (just fill in what's blank)

but what's even worse, when it comes to the FRAME END detection, I actually look for noone to terminate any further hitbox reading. So once the hitbox object sees move[sprite][10] == noone, it knows there is no more hitbox data to read. Its quite poor and I know better ways to handle it now when I restructure it.

I wouldn't see it as much as creating an object for each hitbox data, rather as creating an object instance for each hitbox data. Kind of like you don't create a new object asset for each inventory item, but instead create a new item definition instead (or maybe you create an object; that also sort of depends on what the inventory means).
Right, that's what I understand too. Though I responded that way because Samspade said the concept is the same. If you think it wouldn't make sense to create an actual object for each data type, it wouldn't make sense to us structs.

E.g. you may want to consider a function like createHitboxFrame(start, end, damage, knockbackStrength, ...) if
That is exactly what I plan on wanting to do. Readablility of what it is currently is atrocious as you pointed out

Now onto the code part

1. There's something called "charAnimation.attack0". I don't know if it's a number from 0 to 29, or a sprite index, or something else altogether. But what if it was a struct instead? Something like { sprite: spr_Ninja_attack0, moveInfo: ... }? Where hitboxes would be this whole array at first, maybe, until we refactor it further?
Its an enum for the character's complete animation table. charAnimation.attack0 is 62. This way when attack0 animation is set (when I set sprite to 62) it pulls up the array value of [62,x] for the move's data so I can easily just reference the sprite number to pull the correct hitbox data. It seemed best at the time but I know there is a better way to do it now

moveInfo[0] = b; // is it a sprite or index or what?
Its an index that is used to determine the sprite that is used through a lookup table. Another aspect that is unnecessarily convoluted and needs reform, not to mention I'm wasting array memory by not using 0-61 since attack0 is animation index 62 on the character's animation table. Upon hitbox creation its used for this:

GML:
// hitbox initialize code
hitbox[i+charAnimation.attack0] = asset_get_index("char"+string(character)+"_hitbox"+string(i)); // attack0-29

// hitbox create
hitbox_id.sprite_index = hitbox[move[sprite][0]]; // load hitbox sprite (shared code)
2. There's awfully lot of move[b] references. How about making it var moveInfo = move[b]; or - per earlier suggestion - var moveInfo = charAnimation.attack0.moveInfo? Maybe there's a more accurate name for it than moveInfo? That's all the more reason I'd want to rename "move" to "moveInfo", even if the former is technically shorter; at least with a descriptive name I have a general idea of what I'm dealing with.
That's a great suggestion! Making it var moveInfo[charAnimation.attack0] is so much better for readability sake.

3. Right now, moveInfo is a massive flat array. How about nesting the arrays instead? Since GM:S 2.3+ jagged arrays are perfectly usable (with chained accessors working properly), so you are not limited to grid-like 2D arrays anymore. With this, the moveInfo array can be somewhat simplified to something like:
That's exactly what I was thinking about doing instead, nesting things though I was thinking about doing so as a ds_list since I create a ds_list in the hitbox object, I thought creating the original list as ds_list is better to simply pass the reference maybe? Or maybe keeping it as an array? What I currently do when the hitbox_object is created is convert this 2D array into a single ds_list that way when its time for the next hitbox data to be read, all I have to do is delete one entry from it to shift everything down

GML:
           ds_list_delete(hit_data,10); // prepare next hitbox data by shifting values up 
 
           hitbox_data[10] = ds_list_find_value(hit_data,10); // overwrite with new first frame of current hitbox
           hitbox_data[11] = ds_list_find_value(hit_data,11); // overwrite with new first frame of next hitbox
           hitbox_data[20] = ds_list_find_value(hit_data,20); // overwrite with new last frame of current hitbox
           hitbox_data[30] = ds_list_find_value(hit_data,30); // overwrite with new hitbox damage
           hitbox_data[40] = ds_list_find_value(hit_data,40); // overwrite with new hitbox knockback strength
           hitbox_data[45] = ds_list_find_value(hit_data,45); // overwrite with new hitbox knockback scaling strength
           hitbox_data[50] = ds_list_find_value(hit_data,50); // overwrite with new hitbox knockback angle
           hitbox_data[55] = ds_list_find_value(hit_data,55); // overwrite with new hitbox knockback scaling angle
           hitbox_data[60] = ds_list_find_value(hit_data,60); // overwrite with new hitbox stun type 
           hitbox_data[70] = ds_list_find_value(hit_data,70); // overwrite with new hitbox element type
           hitbox_data[80] = ds_list_find_value(hit_data,80); // overwrite with new hitbox hit sfx
           hitbox_data[90] = ds_list_find_value(hit_data,90); // overwrite with new hitbox whiff sfx
           hitbox_data[100] = ds_list_find_value(hit_data,100); // overwrite with new hitbox hurt sfx
           hitbox_data[110] = ds_list_find_value(hit_data,110); // overwrite with new hitbox grunt sfx 
           hitbox_data[120] = ds_list_find_value(hit_data,120); // overwrite with new hitbox shield stun
           hitbox_data[130] = ds_list_find_value(hit_data,130); // overwrite with new hitbox shield knockback
           hitbox_data[140] = ds_list_find_value(hit_data,140); // overwrite with new hitbox hitstun
           hitbox_data[150] = ds_list_find_value(hit_data,150); // overwrite with new hitbox hitlag
           hitbox_data[160] = ds_list_find_value(hit_data,160); // overwrite with new hitbox cam shake

4. Or maybe make moveInfo items straight-up named? Aside from going all struct-based, you can also used arrays with its indices encoded in enums for readability and easier refactoring. The latter is better for performance-critical applications, but if you aren't accessing hitbox data hundreds or thousands times per Step the struct overhead will be probably negligible.
Hmmm...I'm a little confused how naming things will make it easy to loop through things unless either the names themselves are arrays (like I think you mentioned). It might be a good idea to start with bullet point 3 first, then convert to that more struct method so I go slowly and understand it better?

There's a lot more I could talk about and questions to ask but this post is already full with too much information. I'll wait until you digest my answers to your questions before adding more information and questions to get your thoughts. Can you tell this system is 6 years old with 6 years less experienced programmer me? Its completely awful and bloated and is going to take quite the effort to refactor, but it'll be well worth it!
 
Last edited:

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
Well, I suppose if the code is 6 years old, it's pretty understandable it looks the way it is, especially with certain GML features not existing or being new back then.
By the way, is it the thing in your signature? If so, the backgrounds and character placeholders are pretty neat.

Its an enum for the character's complete animation table. charAnimation.attack0 is 62. This way when attack0 animation is set (when I set sprite to 62) it pulls up the array value of [62,x] for the move's data so I can easily just reference the sprite number to pull the correct hitbox data.
Ah, alright, if charAnimation.attack0 is an enum, then obviously we can't store extra stuff in its variables. And yeah, having a value of attack0 as array index, with previous 62 indices unused (0-61) is pretty suboptimal and unintuitive.

In such case, I'd make "move" a ds_map instead of an array, with the charAnimation entries as keys. So you could do e.g. something like:
GML:
// prepare the moves array
var attack0move = array_create(<length>);
attack0move[0] = charAnimation.attack0;
attack0move[1] = HitboxType.Single; // you might really want to store this single/multiple information in an enum
attack0move[2] = 34;
attack0move[3] = 3;
attack0move[4] = AttackType.Melee; // same for Melee/Projectile distinction
...
// add the moves array/struct to the ds_map
move[? charAnimation.attack0] = attack0move;
I thought creating the original list as ds_list is better to simply pass the reference maybe? Or maybe keeping it as an array?
Nowadays, I'd rather use arrays + structs when applicable, rather than ds_list + ds_maps, especially with the former sparing me the effort of cleanup code and being easier to create with the simple syntax like [1, 2, 3] or {a: 12, b: 34}.

When you're just reading the arrays, then passing the array reference around shouldn't be an issue. When you're writing to the arrays, it's a muddier area (GM:S seems to still have this ugly habit of passing arrays by value to scripts? I dunno), but still workable. And with GM:S 2.3.1 (currently in beta) introducing functions like array_push/array_pop/array_delete, arrays become almost as good at handling dynamic sequences of variables as ds_lists.

Also, having it as an array/struct has an added advantage of storing a type information (I can check for it with is_array/is_type, while ds_list/ds_map indices are plain numbers right now). It might not be that useful in your specific case, but can be handy elsewhere.

The ds_map suggested above is an exception, because it's good for keying by arbitrary number values, as opposed to structs (which can only key by names) and arrays (which key by a series of consecutive indices).

Hmmm...I'm a little confused how naming things will make it easy to loop through things unless either the names themselves are arrays (like I think you mentioned).
Why do you need to loop through things in the first place? Maybe you can rearrange things in a way that naturally handles by-name access, rather than by-index access?

I have no knowledge about the codebase, so I don't know how specific values from the array are being used. However, by the appearance of the data, I'm almost certain it doesn't belong to the fully array-like structure.
Semantically speaking*, the purpose of an array is to store a sequence of somewhat related/similar items. Like, an array of checkpoint times, or an array of cards in hand, or an array of items in inventory. I don't see such a clear theme in the move information array - there seem to be animation info, melee/particle distinction, some sequences of different per-frame informations etc.

With such a variety of items structures and purposes, I feel it's more semantically appropriate to represent it as a struct rather than an array. That's also the idea about my struct-based definition: you have sprite/animationIndex, which is very distinct from hitboxType, which is very distinct from iasa, chargeFrame, moveType and so on.
The general assumption is that animationIndex is used in different places than hitboxType and chargeFrame, so a single name-based access is more intuitive than some kind of looping.

On the other hand, index-based access (which may or may not involve looping) seems to be applied in the context of frame starts, frame ends, damage and so on. That's why in point 4 I added array-valued variables for these (though with the given example, they're all only single-item arrays).
I also assumed that you would want to use N-th frame starts alongside N-th frame end, and definitely N-th damage alongside N-th knockback strength and N-th knockback angle; contrast it to using 3rd knockback strength together with 1st knockback angle - there's nothing like that in your game, is there? If N-th values of specific arrays are used together, I figured I might instead make a single array of structs.

Once again - if you need to loop through an animation index together with hitbox type, charge frame etc., then through starts/ends/damages/etc. - all of that in a common loop - then maybe the premise of the loop itself is flawed?
Are you sure all these values with different structures and applications belong to the same loop?
Having the actual code looping through the array would be useful to tell whether the looping is an appropriate approach or if a code based on named access would work better.

*Technically speaking, an array can just store whatever things are put into it - and in certain scenarios, especially performance-critical, it can be desirable compared to entities like structs - but it makes the arrays in question more confusing compared to the sets of named variables.
 

kupo15

Member
Well, I suppose if the code is 6 years old, it's pretty understandable it looks the way it is, especially with certain GML features not existing or being new back then.
By the way, is it the thing in your signature? If so, the backgrounds and character placeholders are pretty neat.
It is indeed the game this code is for. Thank you very much for the compliment and glad you like what you see so far! :)

Why do you need to loop through things in the first place? Maybe you can rearrange things in a way that naturally handles by-name access, rather than by-index access?
Well, perhaps I don't need to loop through things anymore. You are probably right that the premise of needing to loop is flawed

Currently I loop through it when transferring the hitbox data from the player object to the hitbox being created. Here's another confusing and bloated snippet that I'm trying to figure out lol But it shows that I loop through that move list to transfer all the values into a straight up list, then loop through certain parts of the list (like damage) to strengthen it depending on how charged the move is etc...but I guess looping isn't necessary.
GML:
    hit_data = ds_list_create(); // load hitbox data to obj_hitbox created

    for(var i=0;i<165;i++) // load normal move hitbox data to obj_hitbox create
        {
        ds_list_add(hit_data,_other.move[_other.sprite][i]);
        hitbox_data[i] = _other.move[_other.sprite][i] // load hitbox data to obj_hitbox create
        }

    for(var i=30;i<35;i++) // add charge power to hitbox damage
    if hitbox_data[i] != noone
        {
        if _other.charge != 1
        hitbox_data[i] *= _other.charge//*1.38;
    
        hitbox_fresh[i] = hitbox_data[i];
        ds_list_replace(hit_data,i,hitbox_data[i]);
        }
I have no knowledge about the codebase, so I don't know how specific values from the array are being used. However, by the appearance of the data, I'm almost certain it doesn't belong to the fully array-like structure.
Semantically speaking*, the purpose of an array is to store a sequence of somewhat related/similar items. Like, an array of checkpoint times, or an array of cards in hand, or an array of items in inventory. I don't see such a clear theme in the move information array - there seem to be animation info, melee/particle distinction, some sequences of different per-frame informations etc.
Here's an example of how I use a move index by reference in the player object. IASA stands for "interrupt as soon as", meaning that once you are in this state, you are able to perform other actions because the move is considered to be done even though the tail end of the attack animation is continued to be played

Code:
if frame >= move[sprite][2] // iasa
iasa = true;
So if you perform attack0 (sprite index 62), this code dynamically checks the corresponding movedata array index of 62. Attack1 would be 63 etc.. But I guess what I could do is when you perform the move, create a variable called iasaFrame and set the iasaFrame to the value in the move and compare that instead

if frame >= iasaFrame
iasa = true;

Its also used in creating movement for the attacks such as
Code:
switch sprite {
case charAnimation.attack10: if frame == move[sprite][10] // 1st hitbox data
                                             {
                                             hsp = 3.2*facing;
                                             vsp = -9.5;
                                             hitbox_id.layer = global.layer_hitbox_fake; // de-activate hitbox
                                             }
}
This is why I set it up this way so that the index of the move values is tied to the animation index so they sync up that way instead of having to keep track of an attackInd variable. Having everything be 62-71 was easier than having the attack0 animation be 62 but the moveIndex being 0. So the list not only represents data to be transferred to the hitbox object, but its also referenced by the player for how they behave during the move. Its probably too redundant and it would be better to simply reference the data transferred to the hitbox instead. That way I can do this without worrying about crashing

with hitbox
if frame > hitbox_data[iasa]
iasa = true;

6 years ago, I was in an array frenzy always finding ways to use arrays instead of creating a bunch of different variables bc I thought it would be more dynamic and flexible than hard naming variables, hence why you see unnecessary arrays everywhere lol

I also assumed that you would want to use N-th frame starts alongside N-th frame end, and definitely N-th damage alongside N-th knockback strength and N-th knockback angle; contrast it to using 3rd knockback strength together with 1st knockback angle - there's nothing like that in your game, is there? If N-th values of specific arrays are used together, I figured I might instead make a single array of structs.
Correct, I would never mismatch different Nth values, they are always paired together throughout the entire 164 length list (except for index 0-9 which are global attributes to all hitboxes in that move).

In such case, I'd make "move" a ds_map instead of an array, with the charAnimation entries as keys. So you could do e.g. something like:
Hmmm that could be an idea. I do have a list of animation names as a string I could use that as a reference.
GML:
animationNames[charAnimation.idleStand] = "idleStand"; // idle stand
    animationNames[charAnimation.strafeBack] = "strafeBack"; // walk back
    animationNames[charAnimation.strafeForward] = "strafeForward"; // walk forward
    animationNames[charAnimation.crouchIdle] = "crouch"; // crouch
    animationNames[charAnimation.attack0] = "attack0"; // attack0
    animationNames[charAnimation.attack1] = "attack1"; // attack1

/// and so on
So when I go to retrieve the correct move data from the ROOT map, I could use this array to search for the key?
 
Last edited:

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
So when I go to retrieve the correct move data from the ROOT map, I could use this array to search for the key?
You don't have to - the cool thing about ds_map is that it accepts numeric and string keys alike. Didn't try it with array-valued keys and have no intent of - this would be just weird.
Anyway, you wouldn't need to do something like move[? animationNames[charAnimation.idleStand]]. You can just refer to move[? charAnimation.idleStand] directly, because a ds_map can store by numeric keys without problem.

Correct, I would never mismatch different Nth values, they are always paired together throughout the entire 164 length list (except for index 0-9 which are global attributes to all hitboxes in that move).
In such case, indices 0-9 would belong to the main structure, while indices 10-164 would correspond to groups of individual hitbox parameters. That would also correspond to the structure I came up with:
GML:
var moveInfo = {
    // variables applicable to the global movement
    animation: charAnimation.attack0,
    hitboxType: HitboxType.SingleHit,
    attackType: AttackType.Melee,
    chargeFrame: 3,
    iasa: 34,

    // variables applicable to the individual hitboxes
    hitboxes: [
        {
            frameStart: 4, frameEnd: 11, damage: 16,
            knockback: { strength: 8.1, angle: 10, angleScale: 5, type: KnockbackStunType.FrontRandom },
            hitSfx: HitSfxType.Punch, whiffSfx: WhiffSfxType.Normal, hurtSfx: HurtSfxType.WilhelmScream /* I don't know why there's 2.5 hurt SFX type, though */
        }
        // add more hitboxes to this array if needed
    ]
};
For the record, all these grunt SFX and shield stun parameters and other parameters that were all "noone" - I didn't include them in the struct. You can just check for variable_struct_exists(currentHitbox, "gruntSfx") or something like that, and play the SFX only if currentHitbox actually contains one.
(currentHitbox here would be the variable containing the current specific hitbox information, as calculated from frameStart/frameEnd, I presume)

Those would be the basic, but apparently there are more complex things going behind the scenes, like setting a bunch of variables when starting the first hitbox sequence in the attack10 animation.

But yeah, with structure like this, stored in the currentMove variable, we can write code like:
GML:
if (frame >= currentMove.iasa)
    iasa = true;
GML:
if (frame == currentMove.hitboxes[0].frameStart) {
    hsp = 3.2*facing;
    vsp = -9.5;
    hitbox_id.layer = global.layer_hitbox_fake; // de-activate hitbox
}
Though in the latter case we might instead want to wrap that logic in some kind of method, and associate it with either hitbox itself or the "attack10" character animation group?
 

kupo15

Member
You don't have to - the cool thing about ds_map is that it accepts numeric and string keys alike. Didn't try it with array-valued keys and have no intent of - this would be just weird.
Anyway, you wouldn't need to do something like move[? animationNames[charAnimation.idleStand]]. You can just refer to move[? charAnimation.idleStand] directly, because a ds_map can store by numeric keys without problem.
Neat that makes things easier! So for the moment, it will contain keys from 62-91 instead of 0-29 I guess which is okay. Better than being an array and wasting 0-61

In such case, indices 0-9 would belong to the main structure, while indices 10-164 would correspond to groups of individual hitbox parameters. That would also correspond to the structure I came up with:
I like that structure a lot. It basically looks like a JSON file which is what I was thinking about going for with the ds_list method. And I'll be able to add as many hitboxes as each move needs instead of being limited to 5. I like how you grouped the KB info together as well. Is there a reason you did, organizational preference? If so I might not group them purely for the convenience of not needing another Dot operator to grab those values. A couple questions:

-Where does moveInfo get assigned to since its a local var? Would that be added to the move map?

GML:
var moveInfo = {
    // variables applicable to the global movement
    animation: charAnimation.attack0,
    hitboxType: HitboxType.SingleHit,
    attackType: AttackType.Melee,
    chargeFrame: 3,
    iasa: 34,

    // variables applicable to the individual hitboxes
    hitboxes: [
        {
            frameStart: 4, frameEnd: 11, damage: 16,
            knockback: { strength: 8.1, angle: 10, angleScale: 5, type: KnockbackStunType.FrontRandom },
            hitSfx: HitSfxType.Punch, whiffSfx: WhiffSfxType.Normal, hurtSfx: HurtSfxType.WilhelmScream /* I don't know why there's 2.5 hurt SFX type, though */
        }
        // add more hitboxes to this array if needed
    ]
};

move[? charAnimation.attack0] = moveInfo;
-How do I get information on the KB strength for example? Like this?
GML:
// attack0 starts
currentMove = move[? charAnimation.attack0];

// where ever I need to get hitbox values from
var hitbox0strength = currentMove.hitboxes[0].knockback.strength;
-How do I find out how many hitbox entries their are in that array? array_length(currentMove.hitboxes) ??

For the record, all these grunt SFX and shield stun parameters and other parameters that were all "noone" - I didn't include them in the struct. You can just check for variable_struct_exists(currentHitbox, "gruntSfx") or something like that, and play the SFX only if currentHitbox actually contains one.
(currentHitbox here would be the variable containing the current specific hitbox information, as calculated from frameStart/frameEnd, I presume)
Sounds good! currentHitbox would probably just be set to 0 upon attack start then incremented by one when frameEnd occurs. Also, the sfx = 2.5 was my strange way to code "randomize between the only two grunts I currently have." Setting it to 0 specified grunt0, 1 was grunt1, 2.5 was random lol Now that I have enums, I can simply make random a value that actual makes sense lol

Though in the latter case we might instead want to wrap that logic in some kind of method, and associate it with either hitbox itself or the "attack10" character animation group?
Sorry I don't think I follow what you are asking. I know very little about methods. Is it necessary to tie it to a particular thing instead of doing what you did there? That code is run by the player by the way and is only valid if you are still in the move and not, say, hit out of it. That's why its tied to sprite index. I was thinking about doing this instead for that:

GML:
if (frame == hitbox_id.frameStart) {
    hsp = 3.2*facing;
    vsp = -9.5;
    hitbox_id.layer = global.layer_hitbox_fake; // de-activate hitbox

// hitbox_id being the id of the created hitbox object when you start your move. Since all the data is
// transferred to that object, I can simply reference the value from the object??
}
WilhelmScream
šŸ˜‚
 
Last edited:

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
I like how you grouped the KB info together as well. Is there a reason you did, organizational preference? If so I might not group them purely for the convenience of not needing another Dot operator to grab those values.
Yeah, it's pretty much organizational preference. Also, the fact that I might replace the knockback struct specifically with something like createKnockbackDefinition(strength,angle,angleScale,stun) or something, but this would mostly become advantageous when you would have different ways to define knockback or something.
At this point, neither grouping nor ungrouping has significant practical advantages compared to each other, it boils down to the preference.

-Where does moveInfo get assigned to since its a local var? Would that be added to the move map?
Yup, just the way you did it.

-How do I get information on the KB strength for example? Like this?
That's one way. You may also want to keep track of something like currentHitbox based on the current frame, and it becomes currentHitbox.knockback.strength (or knockbackStrength, if ungrouped) instead.

-How do I find out how many hitbox entries their are in that array? array_length(currentMove.hitboxes) ??
Yes.

Sorry I don't think I follow what you are asking. I know very little about methods. Is it necessary to tie it to a particular thing instead of doing what you did there?
It sort of depends on the mechanics, I suppose? Note that I was addressing not the attack10-entering code itself, but rather the switch/case surrounding it.

Sometimes, it makes more sense to replace switch/case with a method call. Consider this contrived example:
GML:
// greeter definitions
global.basicGreeter = { type: GreeterType.Basic, ... };
global.hipGreeter = { type: GreeterType.Hip, ... };
global.hotelGreeter = { type: GreeterType.Hotel, ... };

// greeting code
switch (currentGreeter.type) {
    case GreeterType.Basic:
        show_debug_message("Hello, " + greetee + "!");
        break;
    case GreeterType.Hip:
        show_debug_message("Yo, " + greetee + "!");
        break;
    case GreeterType.Hotel:
        show_debug_message("Greetings, " + greetee + ".");
        show_debug_message("I hope you enjoy your stay here.");
        break;
}
You can instead opt for method calls, with each method defined in greeter itself:
GML:
// greeter definitions
global.basicGreeter = {
    type: GreeterType.Basic,
    greet: function(greetee) { show_debug_message("Hello, " + greetee + "!"); },
    ...
};
global.hipGreeter = {
    type: GreeterType.Basic,
    greet: function(greetee) { show_debug_message("Yo, " + greetee + "!"); },
    ...
};
global.hotelGreeter = {
    type: GreeterType.Basic,
    greet: function(greetee) {
        show_debug_message("Greetings, " + greetee + ".");
        show_debug_message("I hope you enjoy your stay here.");
    },
    ...
};

// actual greeting, no more switch/cases
currentGreeter.greet(greetee);
The general idea behind this is that it's cleaner to define greeter behaviour in greeter itself, rather than put all the greeting in an external code that tries to switch on each greeter type in existence - and if you add a new greeter type, you need to remember to update the switch/case as well.

However, this mostly works if the behaviour is specific to the greeter itself and nothing else. If e.g. each character enters first phase of attack10 with a completely different code, then the original switch/case might be somewhat better.
 

kupo15

Member
That's one way. You may also want to keep track of something like currentHitbox based on the current frame, and it becomes currentHitbox.knockback.strength (or knockbackStrength, if ungrouped) instead.
Oh so I could assign currentHitbox to be moveCurrent.hitboxes[hitboxIndex] for example, it'll just be shorthand putting the path of the hitbox segment into a variable. Might do that then.
The general idea behind this is that it's cleaner to define greeter behaviour in greeter itself, rather than put all the greeting in an external code that tries to switch on each greeter type in existence - and if you add a new greeter type, you need to remember to update the switch/case as well.

However, this mostly works if the behaviour is specific to the greeter itself and nothing else. If e.g. each character enters first phase of attack10 with a completely different code, then the original switch/case might be somewhat better.
Got it, so I would put the movement code in a method and just define all of them that way and dynamically call them. Just a cleaner way to do it. I could do that too bc that switch statement code runs all the time the only check being the sprite assigned to the character. There are several places where when I add a new character I have to remember to add them to a switch statement and its a pain so this is something I'll have to look into.

I started converting this last night and ran into a question about an attribute I forgot to mention. Some attacks have "heavy" or "stronger" versions of them if you use heavy kick/punch instead of light. Strength increases, or angles change etc... how would I set this is and access it so its easy to pull from those instead? Take this attack:

-The normal version is a 2 hit attack, the strong version is a 3 hit attack. Lots of things change with the strong version including IASA, movetype...etc...

GML:
attackData[4] = {
    // variables applicable to the global movement
   
    animation: charAnimation.attack4,
    moveType: hitboxType.multi, heavyMoveType: hitboxType.multiSingle,
    iasa: 37, heavyIasa: 48,
    chargeFrame: undefined,
    isProjectile: false,
    child: true,

    // variables applicable to the individual hitboxes
    hitboxes: [
        {// hitbox0
            frameStart: 9,
            frameEnd: 13,
            damage: 4, heavyDamage: 11,
            strength: -6.5, heavyStrength: 6.5,
            //strengthScale: 4,
            angle: 52, heavyAngle: 30,
            //angleScale: undefined,
            stun: -11, heavyStun: 20,
           
            hitSfx: 4, heavyHitSfx: 5,
            whiffSfx: 4, heavyWhiffSfx: 3,
            hurtSfx: undefined, heavyHurtSfx: 1,
            //gruntSfx: undefined,
           
            shieldKb: 8.3, heavyShieldKb: 8.6,
            hitstun: undefined
        },
        {// hitbox1
            frameStart: 21,
            frameEnd: 25,
            damage: 10, heavyDamage: 11,
            strength: 6, heavyStrength: 6.5,
            //strengthScale: 4,
            angle: 35, heavyAngle: 30,
            //angleScale: undefined,
            stun: 10, heavyStun: 20,
           
            hitSfx: 4, heavyHitSfx: 5,
            whiffSfx: 3, heavyWhiffSfx: 3,
            hurtSfx: undefined, heavyHurt: 1,
            //gruntSfx: undefined,
           
            shieldKb: 8.3, heavyShieldKb: 8.6,
            hitstun: 19
        },
        {// hitbox2
            frameStart: undefined, heavyFrameStart: 33,
            frameEnd: undefined, heavyFrameEnd: 37,
            damage: undefined, heavyDamage: 11,
            strength: undefined, heavyStrength: 6.5,
            //strengthScale: 4,
            angle: undefined, heavyAngle: 30,
            //angleScale: undefined,
            stun: undefined, heavyStun: 20,
           
            hitSfx: undefined, heavyHitSfx: 5,
            whiffSfx: undefined, heavyWhiffSfx: 3,
            hurtSfx: undefined, heavyHurtSfx: 1,
            //gruntSfx: undefined,
           
            shieldKb: undefined, heavyShieldKb: 8.6,
        }
        ]
    };

What I did with the old version was have another array for special moves and overwrite the data with that if the character triggered the heavy version. Basically just loop again and replaced

GML:
// snippet from the same move
// DAMAGE
                move[b][30] = 4; // hitbox 1 damage
                move[b][31] = 10; // hitbox 2 damage
                move[b][32] = noone; // hitbox 3 damage
                move[b][33] = noone; // hitbox 4 damage
                move[b][34] = noone; // hitbox 5 damage

                    move_special[b][30] = 11;
                    move_special[b][31] = 11;
                    move_special[b][32] = 11;

// how I transferred the data into obj_hitbox
    for(var i=0;i<165;i++) // load normal move hitbox data to obj_hitbox create
        {
        var dat_value = creator.move[creator.sprite][i];
       
        ds_list_add(hit_data,dat_value);
        hitbox_data[i] = dat_value; // load hitbox data to obj_hitbox create
        }

    if creator.special_move // alter for special move hitbox data to obj_hitbox create
    for(var i=0;i<135;i++)
        {
        if creator.move_special[creator.sprite][i] != noone
            {
            hitbox_data[i] = creator.move_special[creator.sprite][i];
            ds_list_replace(hit_data,i,hitbox_data[i]);
            }
        }

Lastly, how would I best put this new struct version data into the hitbox object since I can no longer do loops? Is it now simply passing the reference to the struct to the hitbox then I can use the DOT operator to grab the things I need?
GML:
// attack started in obj_player
currentMove = moveGroup[? sprite];

// passing it to the hitbox_obj
hitbox_id.hitboxData = currentMove;

// using data inside hitbox
if hitboxData.hitboxes[0].frameStart
// start hitbox..
The way I planned the hitbox object is have all the calculations, modifications to the data done prior to going into the hitbox object that way I don't have to worry about any ifs or modifications inside the hitbox. It just contains the final data in it, not both light and heavy versions of the attack
 
Last edited:

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
I started converting this last night and ran into a question about an attribute I forgot to mention. Some attacks have "heavy" or "stronger" versions of them if you use heavy kick/punch instead of light. Strength increases, or angles change etc... how would I set this is and access it so its easy to pull from those instead?
I guess with this kind of functionality, having some kind of struct merging might be actually useful.

One way I could approach that is to store the heavy version separately, kind of like you did in a separate array move_special. The heavy version would contain basically all move attributes as the original, so it'd be almost like copy-pasting the original move and changing the variables appropriately, maybe adding a move here and there.

Except! On definition level, we would actually use a function that only accepts a struct with a corresponding values. Example of how it would work:
GML:
attack_data[4] = { /* regular attack data, like in non-heavy version properties in your code */ };
heavy_attack_data[4] = define_heavy_move(attack_data[4], {
    moveType: hitboxType.multiSingle,
    iasa: 48,
    hitboxes: [
        { damage: 11, strength: 6.5, angle: 30, stun: 30, /* ...and other overwritten properties of the first hitbox */ },
        { damage: 11, strength: 6.5, angle: 30, stun: 30, /* ...and other overwritten properties of the second hitbox */ },
        { /* NEW HITBOX */ frameStart: 33, frameEnd: 37, damage: 11, strength: 6.5, /* ...and so on */ }
    ]
});
The idea is that define_heavy_move(original, overwrites) would be a fully functional, standalone move struct, except it would be based on an existing move and only overwrite some of its properties.

How it would work? More or less, by first copying the original struct, then overwriting properties with values from the provided struct.
Yes, it is looping. A struct-style looping. But don't worry, structs can be looped over, too, if needed.

First, we need a deep_copy(source) function and struct_assign(target, source) function:
GML:
// returns a deep copy of the given value
// for structs and arrays, it's a separate struct/array with the same-shaped values/items; nested structs/arrays are separate as well
// for other values, such as strings, numbers, data structures indices, surface indices etc. the copy is just the value itself
function deep_copy(_source) {
    if (is_struct(_source)) {
        // create a copy of a struct, with all nested structs/arrays also copied
        var _keys = variable_struct_get_names(_source);
        var _length = array_length(_keys);
        var _result = {};
        for (var i = 0; i < _length; i++) {
            var _value = deep_copy(variable_struct_get(_source, _keys[i]));
            variable_struct_set(_result, _keys[i], _value);
        }
        return _result;
    } else if (is_array(_source)) {
        // create a copy of an array, with all nested structs/arrays also copied
        var _length = array_length(_source);
        var _result = array_create(_length);
        for (var i = 0; i < _length; i++) {
            _result[i] = deep_copy(_source[i]);
        }
        return _result;
    } else {
        // if not a struct or an array, just return the simple value
        return _source;
    }
}
GML:
// shallow-copies given values from source struct to the target, i.e. adds or overwrites given target values with source ones
// if no array of values is given, all source values are used instead
function struct_assign(_target, _source) {
    var keys = variable_struct_get_names(_source);
    var _keys_length = array_length(_keys);
    for (var i = 0; i < _keys_length; i++) {
        var _key = _keys[i];
        variable_struct_set(_target, _key, variable_struct_get(_source, _key));
    }
}

Then, we can use these to make define_heavy_move(original, overwrites):
GML:
function define_heavy_move(_original, _overwrites) {
    var _result = deep_copy(_original);

    // this is almost the same as struct_assign(...); the "hitboxes" overwrite being the almost
    var _overwritten_keys = variable_struct_get_names(_overwrites);
    var _overwrites_length = array_length(_overwritten_keys);
    for (var i = 0; i < _overwrites_length; i++) {
        var _key = _overwritten_keys[i];
        // handling basic globals
        if (_key == "hitboxes") {
            __apply_heavy_move_hitboxes(_result, _overwrites.hitboxes);
        } else {
            variable_struct_set(_result, _key, variable_struct_get(_overwrites, _key));
        }
    }
}

function __apply_heavy_move_hitboxes(_result, _hitboxes) {
    var _length = array_length(_hitboxes);
    var _original_length = array_length(_result.hitboxes);
    for (var i = 0; i < _length; i++) {
        var _new_hitbox = _hitboxes[i];
        if (i < _original_length) {
            // overwrite properties of original hitbox
            struct_assign(_result.hitboxes[i], _new_hitbox); 
        } else {
            // add a new hitbox
            // array_set seems to avoid the issues of by-value/by-reference array manipulation
            array_set(_result.hitboxes, i, _new_hitbox);
        }
    }
}

Note: I've written these functions raw without testing, and don't guarantee these work. Hopefully, the code should be easy enough to tweak to get the desired results at this point.

Lastly, how would I best put this new struct version data into the hitbox object since I can no longer do loops? Is it now simply passing the reference to the struct to the hitbox then I can use the DOT operator to grab the things I need?
You can do loops - as demonstrated above - but you don't need to. It's just as you say: you simply pass the reference of the move definition struct - either from attack_data or heavy_attack_data collection - and then access specific properties with the dot operator.
 

kupo15

Member
That's a lot of code for me to figure out how its working lol Forgive me if I don't follow and am misunderstanding some things though your example has helped me understand more things about structs and how to achieve this. So with your method, I would have to separate out the heavy move from the normal ones? Putting the variables side by side like I did seems like it would be easier to read perhaps?

In which case, thoughts on hardcoding it a bit more like this so I can put them all together?

Adapted Scripts
GML:
function heavy_overwrite(structId,key) {

var heavyKey_str = "heavy_"+key;

// if the heavykey value exists
if variable_struct_exists(structId,heavyKey_str)
    {
    var heavyValue = variable_struct_get(structId,heavyKey_str); // get the heavyKey value
    variable_struct_set(structId,key,heavyValue); // overwrite the normal value with it
    }

}
GML:
function apply_heavy_move_data(structId) {

var attributeKeys = ["animation","moveType","iasa","chargeFrame","isProjectile","child"];

// loop through shared attribute keys
var attributeNum = array_length(attributeKeys);
for(var i=0;i<attributeNum;i++)
heavy_overwrite(structId,attributeKeys[i]);

var hitboxKeys = ["frameStart","frameEnd","damage","strength","strengthScale","angle","angleScale","stun","hitSfx","whiffSfx","hurtSfx","gruntSfx","shieldKb","hitstun"];

// loop through number of hitboxes and keys in each hitbox
var hitboxKeysNum = array_length(hitboxKeys);
var hitboxNum = array_length(structId.hitboxes); // number of hitboxes
for(var i=0;i<hitboxNum;i++)
for(var n=0;n<hitboxKeysNum;n++)
heavy_overwrite(structId.hitboxes[i],hitboxKeys[n]);
}


Then I can simply do

GML:
// snippet of hitbox data from before
var attackData[4] = {
    // variables applicable to the global movement

    animation: charAnimation.attack4,
    moveType: 1, heavy_moveType: 2,
    iasa: 37, heavy_iasa: 48,
    chargeFrame: undefined,
    isProjectile: false,
    child: true,

    // variables applicable to the individual hitboxes
    hitboxes: [
        {// hitbox0
            frameStart: 21,
            frameEnd: 25,
            damage: 10, heavy_damage: 11,
            strength: 6, heavy_strength: 6.5,
            //strengthScale: 4,
            angle: 35, heavy_angle: 30,
            //angleScale: undefined,
            stun: 10, heavy_stun: 20,
           
            hitSfx: 4, heavy_hitSfx: 5,
            whiffSfx: 3, heavy_whiffSfx: 3,
            hurtSfx: undefined, heavy_hurtSfx: 1,
            //gruntSfx: undefined,
           
            shieldKb: 8.3, heavy_shieldKb: 8.6,
            hitstun: 19
        }
        ]
    };

// then when I go to access the data and transfer it to the hitbox_obj

var attackDataCopy = deep_copy(attackData);    // create a copy

if heavy_move // if heavy version is triggered
apply_heavy_move_data(attackDataCopy);

hitbox_id.hitboxData = attackDataCopy; // transfer to hitbox_obj

I feel like I pretty much did a similar thing you did but taylored to a different initial setup where I can keep the heavy version modifications right next to the normal ones instead of split up
 
Last edited:

Alice

Toolmaker of Bucuresti
Forum Staff
Moderator
Didn't check the code in depth, but at glance it seems to do its job.

As long as you use apply_heavy_move_data to the deep copy of attack move data (which you seem to be doing), rather than the original, it should be good. If it was applied to the original somehow, then the regular attack data would get corrupted, of course.

I guess at this point you have a pretty solid base for all your refactoring needs; there will probably be some errors and fixes along the way, but that's to be expected from reworking a significant part of the codebase. Hopefully you won't encounter any major problems from now on. ^^
 

kupo15

Member
Didn't check the code in depth, but at glance it seems to do its job.

As long as you use apply_heavy_move_data to the deep copy of attack move data (which you seem to be doing), rather than the original, it should be good. If it was applied to the original somehow, then the regular attack data would get corrupted, of course.

I guess at this point you have a pretty solid base for all your refactoring needs; there will probably be some errors and fixes along the way, but that's to be expected from reworking a significant part of the codebase. Hopefully you won't encounter any major problems from now on. ^^
Awesome, thank you so much for all the help and information. Yep I would say I have all the info I need to refactor this huge system in my game. I have other systems much less involved than this one I will probably return back to this thread after I finished refactoring this to ask about similarly. Really appreciate all the guidance! :D
 
Top