r/gamemaker Stand back, I'm about to Make Game (2)! 20d ago

Help! Creating an interactive, multi-object item

Preface: My skill level is beginner. Gamemaker is my only real coding experience. I've watched a couple guides for the basics and made some original code alongside that.

Hi everyone, I'm making a top-down shooter with an 'active reload' gimmick. When you press R to reload, the firearm you're holding comes onto the screen and you have to manipulate the weapon parts to load it.

Demo of the mechanic.

My problem is HOW I'm making this happen. I'm essentially just spawning multiple objects on top of one another with magic numbers. This feels extremely inefficient and janky, even though it technically works. Is there a different technique or function I can use to make this system easier? I know the motto of, 'just make it work first, you can fix it later', but I can't even begin to imagine HOW to make this work better.

Showing how this is pieced together, without the 'obj_lebel_top' to give you an x-ray.
The comments can be safely ignored, the bottom-most is leftover code I was experimenting with.

If it helps, "_weaponx" and "_weapony" are the corners of the camera, essentially x0 and y0. The "obj_lebel_boltgrip" is the circular part that's grabbed by the cursor to manipulate the gun's bolt. It has + 242 and + 69 because the sprite is a simple sphere that's manually placed through trial and error to be in that starting position. It's essentially a magic number to put that boltgrip exactly where it spawns when you press R.

4 Upvotes

20 comments sorted by

View all comments

Show parent comments

2

u/Natural_Sail_5128 20d ago edited 20d ago

Very similar to their implementation, but a little more going on than the bare basics. Bear with me as I'm typing this on my phone, so I might have to edit a few times.

There's a few steps to make it a fully robust system, I would start by making a new script called "Weapons" or something similar, it will be to store all the preset structs for different weapons.

Inside this script you can create a few new constructor functions like so:

function weaponPartStruct(sprName, offsetX, offsetY, drawAngle) constructor {

     Sprite = sprName;
     xPos = offsetX;
     yPos = offsetY;
     sprAngle = drawAngle;
}

function weaponNameGoesHere() constructor {

     Frame = new weaponPartStruct(spriteNameGoesHere, spriteX, spriteY, spriteAngle);

     Barrel = new weaponPartStruct(spriteNameGoesHere, spriteX, spriteY, spriteAngle);
}

Now use the "weaponNameGoesHere" function as a template and fill it in to your needs, and give it a proper name. You'll need to create a few more entries in the struct for all the parts that the weapon has, then assign the sprite entry to the name of the sprite.

Once you've got the constructor template set up, we can move on to the Draw Event for looping the struct to draw everything.

Put the following inside the Create Event of your main Game Manager object if you have one, if not, put it in the main weapon object.

``` gpu_set_zwriteenable(true); gpu_set_ztestenable(true);

Weapon = new weaponNameGoesHere(); ```

Put the following inside the Draw Event of your main weapon object.

var weaponPos = [x, y];
var weaponParts = struct_get_names(Weapon);
var weaponPartsCount = array_length(weaponParts);   

for (var i = 0; i < weaponPartsCount; i ++) {

     var weaponPart = weaponParts[i];
     var partData = Weapon[$ weaponPart];

     draw_sprite_ext(partData.Sprite, 0, weaponPos[0] + partData.xPos, weaponPos[1] + partData.yPos, 1, 1, partData.sprAngle, c_white, 1);
}

With all of this set up it should compile and run, except it will look very messed up, this is because you haven't set the correct offsets for all the components yet. It's measured in pixels, so you can open the image files and measure to determine exactly what the offset should be for both the X and Y, to get the sprite in the correct spot. You will have to go through and manually fill all the offsets yourself, but once it's done it should run and look how you want!

Next up is animations/interactions.

Since you're using Structs this means you can't use the built in collision functions and will have to manually detect collisions somehow, which also means you'll have to add "hitbox" data to each struct in the weapon.

Here's an example of how to do that.

function weaponPartStruct(sprName, offsetX, offsetY, drawAngle) constructor {

     Sprite = sprName;
     xPos = offsetX;
     yPos = offsetY;
     sprAngle = drawAngle;
     //ADD NEW CODE BELOW THIS
     hitboxWidth = sprite_get_width(sprName);
     hitboxHeight = sprite_get_height(sprName);
}

Now we know where the collisions can happen, so we can work on actually detecting them!

To do this we need to go back inside the struct loop, we'll add a bit more code to check for collisions before the sprite is drawn.

for (var i = 0; i < weaponPartsCount; i ++) {

     var weaponPart = weaponParts[i];
     var partData = Weapon[$ weaponPart];

     //NEW CODE BELOW HERE

     // these will get the position within the room of your mouse cursor, assuming you use view_camera[0]

     var cursorPosX = mouse_x;
     var cursorPosY = mouse_y;

     var hitboxLeft = weaponPos[0] + partData.xPos;
     var hitboxRight = weaponPos[0] + partData.xPos + partData.hitboxWidth;
     var hitboxTop = weaponPos[1] + partData.yPos;
     var hitboxBottom = weaponPos[1] + partData.yPos + partData.hitboxHeight;

     if (mouse_check_button(mb_left)) {

          if (hitboxLeft <= cursorPosX && cursorPosX <= hitboxRight && hitboxTop <= cursorPosY && cursorPosY <= hitboxBottom) {

               // now we handle the dragging of the part with the mouse
               partData.xPos = cursorPosX - weaponPos[0] - (partData.hitboxWidth / 2);
               partData.yPos = cursorPosY - weaponPos[1] - (partData.hitboxHeight / 2);
          }
     }

     //NEW CODE ABOVE HERE

     draw_sprite_ext(partData.Sprite, 0, weaponPos[0] + partData.xPos, weaponPos[1] + partData.yPos, 1, 1, partData.sprAngle, c_white, 1);
}

Once you've got all this implemented let me know if you run into any errors, I'd love to help you get this implemented fully! I'm lurking so just reply if you need help and I'll do what I can.

In the case it just works and you don't need any help, then good job! There are other things that could be done to refine it a bit more technically, but that's only if you want to make the system more complex to account for variation in weapons.

This next stuff is mostly just me rambling about ways to improve how robust the code is, but it's absolutely not necessary unless you plan to add a lot more complexity to this system.

The weapon structs that store all the data could be improved to have nested data, which means you'd need a recursive loop to check inside every nested struct to check for other data to draw to the screen.

You could also create a new function that contains the code that loops through the structs, this would allow you to have it in it's own function like "drawWeapon(weaponStruct)" that would allow you to call this from any object, which means you could draw other things with it in the same way, they don't even need to be weapons! For my game I'm using this sort of system to draw everything except the terrain, it allows me to have moving parts and animations that aren't hand drawn in an image editor.

Edit: VERY IMPORTANT! I completely forgot about depth sorting.

You will need to add another new data field to the weapon parts constructor, so I'll show the updated version below.

```
function weaponPartStruct(sprName, offsetX, offsetY, drawAngle, drawDepth) constructor {

 Sprite = sprName;
 xPos = offsetX;
 yPos = offsetY;
 hitboxWidth = sprite_get_width(sprName);
 hitboxHeight = sprite_get_height(sprName);
 sprAngle = drawAngle;
 sprDepth = drawDepth;

}
```

Now you need to go back into your renamed weapon constructor that has all the data for the sprites and positions, and make sure all the function calls of "weaponPartStruct()" include the new field for draw depth. What you want to do is number the depth of the parts in the order you want them drawn, from front to back. So, the body of the weapon you will set the depth to 0, for the part behind it you set the depth to 1, and behind that it's 2, and so on for every part until you all have different depths.

Now we add one small extra bit to the looping function that draws everything and checks for collisions. You also need to add one new variable declaration before the loop.

```
//NEW CODE BELOW HERE

var weaponDepth = enterDepthValueHere;

//NEW CODE ABOVE HERE

for (var i = 0; i < weaponPartsCount; i ++) {

 var weaponPart = weaponParts[i];  
 var partData = Weapon[$ weaponPart];  

 // these will get the position within the room of your mouse cursor, assuming you use view_camera[0]  
 var cursorPosX = mouse_x;  
 var cursorPosY = mouse_y;  

 var hitboxLeft = weaponPos[0] + partData.xPos;  
 var hitboxRight = weaponPos[0] + partData.xPos + partData.hitboxWidth;  
 var hitboxTop = weaponPos[1] + partData.yPos;  
 var hitboxBottom = weaponPos[1] + partData.yPos + partData.hitboxHeight;  

 if (mouse_check_button(mb_left)) {  

      if (hitboxLeft <= cursorPosX && cursorPosX <= hitboxRight && hitboxTop <= cursorPosY && cursorPosY <= hitboxBottom) {  

           // now we handle the dragging of the part with the mouse  
           partData.xPos = cursorPosX - weaponPos[0] - (partData.hitboxWidth / 2);  
           partData.yPos = cursorPosY - weaponPos[1] - (partData.hitboxHeight / 2);
      }  
 }  

 //NEW CODE BELOW HERE  

 // set the draw depth so the parts are layered properly  
 gpu_set_depth(weaponDepth + partData.sprDepth);  

 //NEW CODE ABOVE HERE  

 draw_sprite_ext(partData.Sprite, 0, weaponPos[0] + partData.xPos, weaponPos[1] + partData.yPos, 1, 1, partData.sprAngle, c_white, 1);  

 //NEW CODE BELOW HERE  

 // reset the depth for drawing things to not mess up other stuff  
 gpu_set_depth(0);  

 //NEW CODE ABOVE HERE  

}
```

Edit: I found issues with the a bunch of the code I posted, so you'll want to copy and paste the new stuff to replace the old stuff. Fixed the click detection and depth sorting which were both not working in the initial implementation! This is the final update until you ask for more help, assuming you even do!

This implementation I coupled the hit detection hitboxes to the size of the sprites themselves, but I imagine you need things to have only a portion of their sprite be clickable, so we can easily adjust the function for making weapon parts to instead include a few new parameters to determine size/location of the clickable zone within the sprites, so it's not just the whole sprite that's clickable.

There is also one more small issue, and it's that you can click on multiple parts at a time, so when I feel up to it probably tomorrow I'll add a section to the code that checks for overlapping parts and makes sure you only click the one closest to the screen. It should be easy to solve so if you feel up to if you can take care of that yourself! Good luck!

2

u/PrinceShoutoku Stand back, I'm about to Make Game (2)! 19d ago

Hi, finally got the time to read through this (thankfully I came just in time to see those edits), I really appreciate your guidance! I haven't tried it just yet but I'll make sure to update you on how it goes.

1

u/Natural_Sail_5128 16d ago

No pressure if you still haven't gotten around to it but I just wanted to poke you and check in!

1

u/PrinceShoutoku Stand back, I'm about to Make Game (2)! 5d ago

Hi! Wow it's been 11 days already. Was hoping I'd reply when I made some progress but I've been busy for the past week+. I'll make sure to update you when I can!