1
\$\begingroup\$

Following on from my previous question about optimising large object counts in Gamemaker, I am looking for a method for the solution proposed by liggiorgio.

My game has large numbers of small, sprite-based objects, represent individual strands or clumps of fur. For a variety of reasons, they all need to be interactive objects. At the moment, I am reaching limits on how many I can generate without the game grinding to a halt.

As the game involves a very large (20,000px x 20,000px) room with a restricted viewport, liggiorgio proposed a solution using the built-in Application Surface to only draw objects which are currently within the viewport. This would mean that the vast majority of the room, and its objects, would not be drawn, and hopefully framerates should improve.

Can anybody help me with implementing this?

Update: I am now using this code in an object's Draw event as a test:

if (x > view_xview[0] and x < (view_xview[0] + view_wport[0]))
and (y > view_yview[0] and y < (view_yview[0] + view_hport[0]))
{
draw_self()
} else {
}

However, even when I pan the view over to the affected object, it is not drawn.

Edit: Thanks, liggiorgio, for your below solution. This code works well for drawing and not drawing based on view in the room, as far as I can tell. However, not-drawing the large number of oFur objects does not seem to help with my performance issues. I think that deactivating those not currently in view is the best option.

I know that deactivating instances can be risky, but I don't think it will be a problem for me:

  • I only have one room in my game, so there is no worry about persistent objects not being carried over;
  • At no point will I be trying to delete all of the oFur objects at once;
  • I have created a FurDeactivation control object, so I won't fall into the trap of having the oFur modules try to deactivate themselves and then therefore be unable to reactivate themselves when they are in the view. I made this mistake at first, but I've now corrected it with the FurDeactivation control object.

This is the script that is running in the Step event of the control object. The ObjectType is oFur;

///DeactivateIfOutOfFrame(ObjectType)

ObjectType = argument0

with ObjectType {
if ((x > (view_xview[0]-sprite_width/2)) && (y > (view_yview[0]-sprite_height/2)) && (x < (view_xview[0]+view_wview[0]+sprite_width/2)) && (y < (view_yview[0]+view_hview[0]+sprite_height/2)) )
{
instance_activate_object(self)
} else {
instance_deactivate_object(self)
}    
} 

At the moment, it does not seem to be deactivating any of the instances of oFur outside the view. Any ideas?

\$\endgroup\$
2
\$\begingroup\$

Following on from my edit above, I have worked out how to deactivate the oFur objects and reactivate them only when they are in the current viewport. While the game runs slowly (considering there are ~400,000 oFur objects with about 3000 on screen at any one time) it now, at least, runs. The next steps will be:

  • making each oFur object larger, to cover more of the screen at a time;

  • reduce the number of oFur objects to improve performance:

  • Perhaps make the room itself smaller, to lower the count needed;

  • find a way to generate them more quickly (as the method detailed below takes about 30 seconds to generate all of them).

Solution

An invisible block, mFurBlock, is generated in the top left of the room, and 1,000 oFurs are generated in a random fashion across it. This will serve as the 'seed' block, from which all others are created. If I was to try to generate the fur across the entire room, it would take nearly two hours.

Instead, I copy each oFur in this mFurBlock across the room a number of times corresponding to (WidthOfRoom / WidthOfBlock), which in this case is `20,000 / 1000', which leaves me with 20 'blocks' of fur.

Each time the fur is copied, its x is increased by WidthOfBlock * NumberOfCopiesMade to gradually move them across the room. Therefore, if 4 copies had been made, the next copies would be moved 4000 pixels across the room. This leaves me with an initial row of fur across the top of the room.

The fur block is then copied down the room, and a new row made using the same code. This leads to a 'scanning' effect across the room, with new furs being created and positions in blocks of 1000px x 1000px, row by row.

The key is to, after each copy has been made and positioned, to deactivate those copies. This means that there are never too many instances active (in fact, only the 'seed' block and the current copied block) and the game won't crash. The framerate does drop, but we could hide this behind a static loading screen or something similar. Or, you know, just optimise it.

When the entire room is finished, the 'only active when in viewport' code is activated in a separate 'Control' object, and persists for the rest of the game:

if global.WholeFieldGenerated = true {
if DeactivateToggled = false {
instance_deactivate_object(oFur)
instance_activate_region(view_xview[0], view_yview[0], view_wview[0], view_hview[0], true)
} else if DeactivateToggled = true {
instance_deactivate_object(oFur)
}

This deactivates all oFurs every step, and then activates any deactivated objects in the room that are currently within view.

Below is my commented generation script, so you can see exactly how I did it.

It needs work, but it at least works!

//If the first fur block has been completed, and finished generating...
if CreatedFirstFurBlock = true and mFurBlock.FinishedGenerating = true {

/* CREATING THE FIRST ROW */
if FirstRowCompleted = false {

if NumberOfFurBlocksGenerated < global.TotalDuplicationsPerRow {
with oFurBlockFur {
//If a fur piece is within the FurBlock (i.e. it is in the 'root' block)...
if x <= mFurBlock.sprite_width {
//...Copy it and move it along the row the number of 'spaces' required.
ThisCopy = instance_copy(true)
ThisCopy.x = x + (mFurBlock.sprite_width * mFurGeneration.NumberOfFurBlocksGenerated)
//Deactivate the copy, hiding it from view now that it is positioned. This will not effect the 'root' block, meaning that we can use it again.
instance_deactivate_object(ThisCopy)
}
}
//Increase the number of fur blocks generated.
NumberOfFurBlocksGenerated = NumberOfFurBlocksGenerated + 1
} else if NumberOfFurBlocksGenerated >= global.TotalDuplicationsPerRow {
FirstRowCompleted = true
NumberOfFurBlocksGenerated = 1
}
/* CREATING SUBSEQUENT ROWS */

} else if FirstRowCompleted = true {

if NumberOfRowsGenerated < global.TotalRows {

if RowDone = true {
with oFurBlockFur {
//If the fur is part of our 'root block'...
if y <= mFurBlock.sprite_height and x <= mFurBlock.sprite_width {
//...Copy it and move it down the number of 'rows' required. Mark it as in the root so 
//that we can deactivate it later.
ThisCopyRoot = instance_copy(true)
ThisCopyRoot.InRoot = true
ThisCopyRoot.y = y + (mFurBlock.sprite_height * mFurGeneration.NumberOfRowsGenerated)
//We don't deactivate the copy, as we need it to generate the row. However, we won't deactivate the 'root' block either, as it is useful to use 
//as a generator for subsequent rows. This will only increase the number of 'blocks' active by 1, as each row is deactivated each time they have finished 
//generating.
}
}
RowDone = false
ThisRowBlocksGenerated = 1
}

//Reset the row done function. This allows us to get on with making this new row.



if RowDone = false {
if ThisRowBlocksGenerated < global.TotalDuplicationsPerRow {
with oFurBlockFur {
//If a fur piece is in the 'root block' of the current row...
if y >= (mFurBlock.sprite_height * mFurGeneration.NumberOfRowsGenerated) {
//Copy and move it along, as before.
ThisCopy = instance_copy(true)
ThisCopy.x = x + (mFurBlock.sprite_width * mFurGeneration.ThisRowBlocksGenerated)
instance_deactivate_object(ThisCopy)
}
}
//Update the block count in this row.
ThisRowBlocksGenerated = ThisRowBlocksGenerated + 1

//When we have finished this row, update the number of rows, set the row as done (which allows a new 'root' block to be generated next step)
//and deactivate the 'root' block for this row. Can this last thing be done? I'm not sure in this current setup.
} else if mFurGeneration.ThisRowBlocksGenerated >= global.TotalDuplicationsPerRow {
NumberOfRowsGenerated = NumberOfRowsGenerated + 1
with oFurBlockFur {
if InRoot = true {
instance_deactivate_object(self)
}
}
RowDone = true
}
}

//We now replay this code, moving the 'root block' down via copy, using that to generate a new row, and then deactivating the entire row.


//Once all the rows are generated and deactivated, we can tell the ObjectViewDeactivator that it is ready to shine. This Generation object can then be deleted.
} else if NumberOfRowsGenerated >= global.TotalRows {
NumberOfFurBlocksGenerated = 0
global.WholeFieldGenerated = true
instance_destroy()
}

}
}
\$\endgroup\$
1
\$\begingroup\$

In GM:S, the variables view_wport[] and view_hport[] refer to the width of the view in the game window, not in the room. Thus, they are used for UI design purposes only. Read more about them on the official documentation page.

What you actually need are the variables view_wview[] and view_hview[], which define the width of a given view inside a room. Try to use this code in the oFur's Draw Event:

// Draw Event
if ( (x > view_xview[0]) && (y > view_yview[0]) && (x < (view_xview[0]+view_wview[0])) && (y < (view_yview[0]+view_hview[0])) )
}
    draw_self();
}
else
{
    // Else statement
}

In addition, you want your objects to be drawn when they intersect the view borders, not just if their origin lies within the view rectangle; so, let's improve the code as follows:

// Draw Event
if ( (x > (view_xview[0]-sprite_width/2)) && (y > (view_yview[0]-sprite_height/2)) && (x < (view_xview[0]+view_wview[0]+sprite_width/2)) && (y < (view_yview[0]+view_hview[0]+sprite_height/2)) )
}
    draw_self();
}
else
{
    // Else statement
}

By the way, if the draw_self() function fails to do its job, you can replace it with other functions such as draw_sprite(), or its advanced version draw_sprite_ext() which gives you much more control over the way sprites are drawn on the Application Surface.

\$\endgroup\$
  • \$\begingroup\$ Thanks liggio; I've edited my original question with an implementation of your code which I cannot get to work. \$\endgroup\$ – Bonfire Dog Jul 19 '16 at 14:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.