Legacy GM The Magic Numbers in the image_index [SOLVED]

Hey,

I have a ballista object that shoots an arrow when if statements are true like below;
Code:
if (state == "ready" && (collision_line(x+24,y-8,x+TowerRange,y-8,oEnemy,true,true)) && canAttack == true ) //I'm using state to say if the ballista can attack or not but I just tried adding another variable..
{
image_speed = 0;
 if ( (ballista.direction == dir) && (ballista.image_index == 10) ) //when I make this part "ballista.image_index == 10" to "ballista.image_index >= 10", it creates multiple objects
 {
   image_index = 0;
   state = "reloading";
 with (instance_create(x+11, y-(55.5), oBallistaArrow)) //Create the arrow x+11,y-(55.5) //(ballista.x-1,ballista.y-4,oBallistaArrow))
   {
    speed = 10;
    direction = other.ballista.image_angle;
    image_angle = other.ballista.direction;
    other.canAttack = false;
   }
 }
}
and this is the only code that effects the image_speed of the ballista object
Code:
if (state == "reloading")
 {
  ballista.image_speed = 0.1;
 }
  else if (state = "ready")
   {
    ballista.image_speed = 0;
   }
The problem is, the image_index of ballista object -which is a variable = instance_create(); type of object- is never equal to bare 10, but higher than 10 like in the screenshot below. So that makes it create multiple arrow objects which I don't want. How to deal with this?
upload_2018-10-15_20-19-42.png
 
Whenever you are not handling whole integers, you have to treat the numbers differently. Because you're making the image_speed so slow, it goes up in increments of less than 1, meaning that there'll be overshoot. The way to fix it is to change:
Code:
&& (ballista.image_index == 10)
To:
Code:
&& (ballista.image_index >= 10)
 
Nah, I don't think it works that way Simon. Since it's past the state check, it'll continue through the rest of the code before running the new code when it hits the state check again and sees that the state is different.
 

Neptune

Member
Seems like using built-in things like image_speed is what causing your issue.
One band-aid fix would be to create another variable, and do all your referencing to that...
Code:
ballista_image = image_index div 1;

if ballista_image == 10
{

}
In the future, you could try animating things like so:
Code:
animation_alarm += 1;
if animation_alarm > 10
{
    index += 1;
}
That way you're only ever working with whole numbers!
 
He just needs to take into account the possibility of overshoot by using >= rather than ==.

I just want to point out that this is GM functioning totally correctly. It's not a bug that image_index is slightly larger than 10. It's a feature of the way you're incrementing the numbers. You can do a bit of faffing about to use whole numbers as Vether suggests, but it's much simpler and more correct coding practice to understand what type of variable you're dealing with and using >= when appropriate and == when appropriate.
 
D

dannyjenn

Guest
I don't know why it would be creating multiple instances.

But to get around the problem with the floating point error, I believe you need to call this:
Code:
math_set_epsilon(0.00001);
(0.00001, because 10.2000026702881 should be 10.2, which means that the error is less than 0.00001)
Call that function at game start, or in the create event, or something. At least I think that's how this function is to be used, though I might be completely wrong here.

Another possible solution would be to change (ballista.image_index == 10) to ((ballista.image_index-10) <= .05) (the .05 doesn't need to be .05, but it does need to be some number less than .1)).

A third possible solution would be to not use 0.1, since 0.1 is not composed of powers of 2. Instead, use some approximation for 0.1, such as 0.09375 (which is composed of powers of 2: 0.09375 is (1/power(2,3))-(1/power(2,5))). I might be wrong about this third solution though.

If none of that works, I don't know what to say. Sorry.

edit - Whoa, ninja'd by a ton of other people at once, lol

edit - Oh, and this might also help: change (ballista.image_index == 10) to ((ballista.image_index^0) == 10). The bitwise xor with 0 will change the 10.2000026702881 to just 10.
 
Last edited by a moderator:
You can use a range, depending on what you're doing, or div the number as you suggested, but the point I'm trying to get across to the original poster is the understanding the difference; if they know when they should be using >= and when they should be using == then it's very simple to build onto that understanding the things that you're suggesting. In other words I think the problem is more hypothetical than practical.
 
Thanks for all the answers, I tried all of them instead creating a variable like
@Simon Gust it does :/

@Vether said, I may do this in a couple hours.

@dannyjenn I tried your first solution but nothing changed, keeping the other solutions in a corner right now.

@RefresherTowel I understand why are you telling me to use ">=" here but it doesn't change anything, still creates multiple objects.
Here is the full Step Event code of that object, if it helps
Code:
dir = ballista.direction;

if ( (collision_line(x+24, y-8, x+TowerRange, y-8,oEnemy,true,true)) && (instance_exists(oEnemy)) )
{
 // set the angle ranges for min and max (the 45 is 360-45)
 var a1 = 28;
 var a2 = 45;
 var achange = 0.5; // not exactly how much the angle changes but the higher it is the faster it changes.

 // set the dir to the mouse angle and set the approprate ranges based on left/right
 var dir = point_direction(x+10, y-53, oEnemy.x, oEnemy.y);
 if (type == "right") { var r1 = a1; var r2 = 360-a2; var target = 360; }
 if (type == "left") { var r1 = 180-a1; var r2 = 180+a2; var target = 180; }

 // check if the mouse is in the range
 var moveTarget = false;
 if (type == "right") moveTarget = (dir < r1 || dir > r2);
 if (type == "left") moveTarget = (dir > r1 && dir < r2);
 // if it is, move to the mouse
 if (moveTarget) {
     with (ballista) {
         var dd = angle_difference(image_angle, dir);
         image_angle -= min(abs(dd), achange) * sign(dd);
     }
 }
 // now see if the turret itself is in the range
 var inTargetRange = true;
 //dir = ballista.direction;
 if (type == "right") inTargetRange = (dir < r1 || dir > r2);
 if (type == "left") inTargetRange = (dir > r1 && dir < r2);
}
 // if it isn't, move into the range
 //if(!inTarget) {
 //    with(ballista) {
 //        var dd = angle_difference(image_angle, target);
 //        image_angle -= min(abs(dd), achange) * sign(dd);
 //    }
 //}
 // set the ballista direction to its image angle

if (ballista.image_index == 10)
 {
  //ballista.image_speed = 0;
  //ballista.image_index = 10;
  state = "ready";
  drawthearrow = true; //this is just a draw_sprite();
  canAttack = true;
 } else
   {
    state = "reloading";
    drawthearrow = false;
   }

if (state == "ready" && (collision_line(x+24,y-8,x+TowerRange,y-8,oEnemy,true,true)) && canAttack == true )
{
image_speed = 0;
 if ( (ballista.direction == dir) && (ballista.image_index == 10) )
 {
   image_index = 0;
   state = "reloading";
 with (instance_create(x+11, y-(55.5), oBallistaArrow)) //Create the arrow x+11,y-(55.5) //(ballista.x-1,ballista.y-4,oBallistaArrow))
   {
    speed = 10;
    direction = other.ballista.image_angle;
    image_angle = other.ballista.direction;
    other.canAttack = false;
   }
 }
}

if (state == "reloading")
 {
  ballista.image_speed = 0.1;
 }
  else if (state == "ready")
   {
    ballista.image_speed = 0;
   }
 
  ballista.direction = ballista.image_angle;
Create Event
Code:
image_speed = 0;
image_index = 10;
y = 166;
cogAngle = 0;

alarm[0] = -1;

canAttack = true;

TowerRange = 192;
BallistaCooldown = room_speed*5; //this is not used

type = "right";
state = "ready";

drawthearrow = true;

if (type == "right")
 {
  ballista = instance_create(x+12,y-50,oBallista);
   ballista.sprite_index = sBallistaRight;
   image_index = 0;
   ballista.image_index = 10;
   cog = instance_create(x+12,y-51,oBallistaCog);
 } else

 if (type == "left")
  {
  ballista = instance_create(x-8,y-55,oBallista);
   ballista.sprite_index = sBallistaLeft;
   image_index = 1;
   ballista.image_index = 10;
   cog = instance_create(x-11,y-51,oBallistaCog);
  }
 
D

dannyjenn

Guest
I don't think that this is part of the problem, but I notice you have other.ballista.image_angle and other.ballista.direction. As far as I know, GameMaker doesn't have that sort of syntax (i.e. you aren't allowed to chain together multiple dot operators like that). Instead, you need to use parentheses:
Code:
direction = (other.ballista).image_angle;
image_angle = (other.ballista).direction;
 
@RefresherTowel I think I've corrected what you meant. Although I didn't actually understand which part you meant
@spoonsinbunnies I did what you said;
Code:
if (ballista.image_index >= 10)
 {
  ballista.image_index = floor(ballista.image_index);
 }
if (ballista.image_index == 10)
 {
  state = "ready";
  drawthearrow = true;
 } else if (ballista.image_index < 10)
   {
    state = "reloading";
    drawthearrow = false;
   }
I changed the last part to this and it only creates 1 arrow object now!
Code:
if ( (state == "ready") && (collision_line(x+24,y-8,x+TowerRange,y-8,oEnemy,true,true)) )
{
 if ( (ballista.direction == dir) && (ballista.image_index == 10) )
 {    
 with (instance_create(x+11, y-(55.5), oBallistaArrow)) //Create the arrow x+11,y-(55.5) //(ballista.x-1,ballista.y-4,oBallistaArrow))
   {
    speed = 10;
    direction = (other.ballista).image_angle;
    image_angle = (other.ballista).direction;
    (other.ballista).image_index = 0;
    other.state = "reloading";
   }  
 }
}
 

TheouAegis

Member
if 10-image_index <= image_speed {

That's a single-fire conditional for incompatible floating point comparisons where the target value (in this case, 10) is higher than the current value. If the image_index is 9.994, then 10-9.994 is 0.006 which is less than the image_speed of 0.1, so the condition is true.

You can avoid all of this crap though by just using fractional values whose denominators are powers of 2 and whose numerators are easily divisible by the target frame.

Examples:

Single target frame of 3
Desired speed is 0.1 or 1/10 frames per second (incompatible)
Divide the target frame by the desired speed to get the number of frames it will take
3/0.1 = 30
Round that value to the nearest power of 2
The nearest powers of 2 to 30 are 16 and 32, so pick 32
Set the target frame as the numerator and use the nearest power of 2 as the denominator
Your new image_speed should be 3/32

Single target frame of 7
Desired speed is 0.3 or 3/10 frames per second
7/0.3 = 23.33
Nearest powers of 2 to 23.33 are 16 and 32
7/16 is 0.1375 faster than our target of 0.3
7/32 is 0.08125 slower than our target of 0.3
New image_speed should be 7/32

As you can see in the second example, this isn't always an ideal method because it can create HUGE disparities between speeds. With that said, it's still a valuable method if you can work around fractional targets instead of integral targets. Consider this next example:

Single target frame of 5
Desired speed is 0.2 or 1/5 frames per second
5/0.2 = 25 frames
Nearest power of 2 is 32
5/32 is 0.15625 which is too slow
Divide 32 by the target frame and multiply that by the target numerator
32/25 * 5 = 6.4
Now the closer speed is 6/32 or 0.1875 frames per second which is still too slow
Since 6.4 is so close to 6.5, double the numerator and denominator then add 1
Now the image_speed should be 13/64 or 0.203125
I think we can both agree that's satisfactorily close enough to the target 0.2 fps
Divide the target frame by the new image_speed, round up, then multiply by the image_speed
The new target frame should be 325/64

Here you see a case where the target frame won't be an integer, because image_speed can never equal that integer.

If you have multiple targets, it's not as simple. You can try to find a common denominator to use as the numerator, but in many cases you'll need to change the target for at least one of them. If each target is a multiple of the other, then try to find a perfect match for the lowest target. For example, a speed of 3/16 will work for all 3, 6, 9, 12, 15, 18; but a speed of 9/32 would only work on frames 9 and 18.
 
Top