Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 16 - Object Autodeletion and (0,0) Spawning

The video for this lesson is Python's Human Inferno, because Mokou likes to regenerate and add extra singles onto plurals with reckless abandon.

Part 1: What will be Covered in this Lesson?

In this lesson, I will discuss how to make single scripts plural-friendly. I will cover TFinalize in greater detail, as well as how Object Autodeletion impacts a script. (0,0) spawning and how to prevent it are major topics in this lesson. Additional miscellaneous topics will also be covered. This lesson assumes that you are familiar with the contents of Lessons 3-8 and Lesson 12. If you are not familiar with these, please review them.

The tl;dr of this entire lesson is do not spawn objects using the position of deleted objects.

Part 2: Why Upgrade Single Scripts?

So far, most of my code examples (exception being in Lesson 6) have been written in such a way that assumes they were mere singles, not linked into boss battles. And so, we have, for the most part, been writing code in such a way where, theoretically, bullets can spawn after the conclusion of the single. We will, in this lesson, tackle this problem.

To highlight this problem, let's consider the TFinalize from Lesson 7. Note that this version does not have the spellcard bonus implemented.

task TFinalize{
    while(ObjEnemy_GetInfo(objBoss, INFO_LIFE) > 0){yield;}
    Obj_Delete(objBoss);
    DeleteShotAll(TYPE_ALL, TYPE_IMMEDIATE);
    SetAutoDeleteObject(true);
    CloseScript(GetOwnScriptID());
}

In the above, we wait for the boss's life to hit 0, and then delete the boss, delete all existing shots, and set Autodelete to true, which will tell Danmakufu to delete all objects created in this script when CloseScript() is run.

Now, let's say that we have a task called Spawn, which is the following:

task Spawn {
    while(ObjEnemy_GetInfo(objBoss, INFO_LIFE) > 0){
        ascent(i in 0..30){
            CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, i*23, 1, 30);
            yield;
        }
        wait(10);
    }
}

Now, let's conjure a scenario. Suddenly, the boss just died. In Spawn, i is now equal to 3, and we are in an ascent loop. One frame later, the boss object no longer exists, and all objects created in the script, including all shots on the screen and all delay clouds, are deleted.

However, the task is still running. i is equal to 4. A bullet spawns as the next single in the plural starts. However, objBoss no longer exists. So the bullet spawns at (0,0), in the top left of the screen.

Admittedly, this scenario becomes more likely if you have as much as a single yield in TFinalize. But now we're going to see what exactly we can do to prevent (0,0) spawning.

Part 3: How Do I Prevent (0,0) Spawning?

Since (0,0) spawning is caused by positioning objects relative to other objects that no longer exist, we can work appropriately to prevent it. In the above case, we had no way to prevent (0,0) spawning - it would simply occur occasionally. But now we will go over how to stop it.

In Lesson 6, I introduced the following line of code:

if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) <= 0){return;}

When placed in a task, the code will check if the life of the boss is greater than 0 (which is the base condition built into Danmakufu used for checking if a single has finished). Alternatively, you can use the following:

if(Obj_IsDeleted(objBoss)){return;}

It really depends on how your script works, but either should work fine.

As for where to place it, I suggest placing it after every yield or wait statement in a task. This way, it will not be checked every frame, but will only be checked before spawning objects, playing sound effects, etc. For example, let us examine the following code:

task Bullets {
    while(ObjEnemy_GetInfo(objBoss, INFO_LIFE) > 0){
        loop(30){
            let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 4, rand(0, 360), 4, 0);
            wait(5);
        }
        wait(10);
    }
}

There is only one place where bullets are fired. Therefore, we should place the check before that statement. Note that by placing the check before the object creation step, it is not necessary to place a check before or after wait(10); because there is no actual benefit to doing so. As a result, our code is as follows:

task Bullets {
    while(ObjEnemy_GetInfo(objBoss, INFO_LIFE) > 0){
        loop(30){
            if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) <= 0){return;}
            let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 4, rand(0, 360), 4, 0);
            wait(5);
        }
        wait(10);
    }
}

Of course, this is only for end-of-single boss lifebar checking. (0,0) spawn errors can occur anywhere in your script where you delete bullets. Let us look at the following example:

task Ring {
    ascent(i in 0..8){
        let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 8, GetAngleToPlayer(objBoss) + i*360/8, DS_BALL_M_RED, 0);
        ascent(j in 0..12){
            Surround(obj, j);
        }
        wait(30);
    }
}

task Surround(parentobj, id) {
    let obj = CreateShotA1(ObjMove_GetX(parentobj), ObjMove_GetY(parentobj), 0, 0, DS_BALL_SS_RED, 0);
    ObjShot_SetAutoDelete(obj, false);
    let objcount = 0;
    loop(60){
        ObjMove_SetPosition(obj, ObjMove_GetX(parentobj) + 30*cos(objcount + id*360/12), ObjMove_GetY(parentobj) + 30*sin(objcount + id*360/12));
        objcount++;
        yield;
    }
    ObjMove_SetSpeed(obj, rand(0.5, 5));
    ObjMove_SetAngle(obj, rand(0, 360));
    ObjShot_SetAutoDelete(obj, true);
}

We have multiple problems in this example. Firstly, the Ring task can continue to fire bullets after the boss is gone, because there is no check in the Ring task. Secondly, what happens if one of the objects created from the Ring task leaves the screen? What happens to the objects created in the Surround task? When the parentobj goes off screen before 60 frames have passed, the objects created by Surround are still rotating around the parent. But when it suddenly disappears, the surrounding bullets may try to set their position once more and, in the process, relocate to (30*cos(objcount + id*360/12), 30*sin(objcount + id*360/12)) since ObjMove_GetX(parentobj) and ObjMove_GetY(parentobj) both return 0.

As you can see, we must fix both situations. The Ring task requires a check to see if the boss is still in existence, but the Surround task requires a check to see if the parent object is still in existence. As a result, we have the following:

task Ring {
    ascent(i in 0..8){
        if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) <= 0){return;}
        let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 8, GetAngleToPlayer(objBoss) + i*360/8, DS_BALL_M_RED, 0);
        ascent(j in 0..12){
            Surround(obj, j);
        }
        wait(30);
    }
}

task Surround(parentobj, id) {
    let obj = CreateShotA1(ObjMove_GetX(parentobj), ObjMove_GetY(parentobj), 0, 0, DS_BALL_SS_RED, 0);
    ObjShot_SetAutoDelete(obj, false);
    let objcount = 0;
    loop(60){
        if(Obj_IsDeleted(parentobj)){return;}
        ObjMove_SetPosition(obj, ObjMove_GetX(parentobj) + 30*cos(objcount + id*360/12), ObjMove_GetY(parentobj) + 30*sin(objcount + id*360/12));
        objcount++;
        yield;
    }
    ObjMove_SetSpeed(obj, rand(0.5, 5));
    ObjMove_SetAngle(obj, rand(0, 360));
    ObjShot_SetAutoDelete(obj, true);
}

Alternatively, we can use the objcount to write Surround differently.

task Surround(parentobj, id) {
    let obj = CreateShotA1(ObjMove_GetX(parentobj), ObjMove_GetY(parentobj), 0, 0, DS_BALL_SS_RED, 0);
    ObjShot_SetAutoDelete(obj, false);
    let objcount = 0;
    while(objcount < 60 && !Obj_IsDeleted(parentobj)){
        ObjMove_SetPosition(obj, ObjMove_GetX(parentobj) + 30*cos(objcount + id*360/12), ObjMove_GetY(parentobj) + 30*sin(objcount + id*360/12));
        objcount++;
        yield;
    }
    ObjMove_SetSpeed(obj, rand(0.5, 5));
    ObjMove_SetAngle(obj, rand(0, 360));
    ObjShot_SetAutoDelete(obj, true);
}

Part 4: How Do I Use SetAutoDeleteObject()?

Now we will have a brief discussion on SetAutoDeleteObject()

As has been stated before, calling this function with the value true will toggle object deletion upon closure of the script it was called in. For instance, let's say I have a Cutin function that has both graphical and text components. The text, at the very least, must stay until the end of the Single. As a result, we can choose to delete it manually when the boss's HP has dropped below 0. However, this condition assumes that there is a boss to begin with, and hardcoding the case results in the function being useless in any other situation.

The standard (not the default), is to use SetAutoDeleteObject(true) before you close a script. It can be used in any form of script and is commonly used in all standard scripts (we will discuss its usage or the lack thereof in packages in a much later tutorial). By default, SetAutoDeleteObject() is set to false.

By using this function, the graphics and text created by the Cutin function will automatically delete at the termination of the parent script, removing any need to manually delete them and allowing the function to be used in any script that uses SetAutoDeleteObject(true). This is a great boon in function libraries, which will be discussed in a few tutorials.

We will discuss objects that must persist after a Single script has terminated in later tutorials, specifically when we discuss NotifyEvent and Items.

In the end, by using this function in conjunction with various other means of preventing (0,0) spawning, you can improve the stability of your script while also making sure that all objects are properly deleted when they are no longer needed.

Part 5: What Other Methods Can I Use?

In this guide (and for the entire tutorial sequence as a whole), we have assumed that you are using a TFinalize task. However, it is possible to use @MainLoop to handle the same functionality. You can use the following at the end of @MainLoop instead of TFinalize:

if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) <= 0){
    Obj_Delete(objBoss);
    DeleteShotAll(TYPE_ALL, TYPE_IMMEDIATE);
    SetAutoDeleteObject(true);
    CloseScript(GetOwnScriptID());
}

Alternatively, you can place a control statement on the yield; at the end of @MainLoop to prevent it from yielding if the boss's HP is less than 0.

if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) > 0){yield;}
//instead of simply using
yield;

Since preventing the yield; in @MainLoop will prevent tasks for running, you can effectively shut down all object creation as long as all of your objects are created in tasks. Note, however, that since tasks will not resume, no tasks will update at all in that particular script, and if you want explosion effects or other effects, you will have to handle them using NotifyEvent or separate Single scripts.

Quiz: Preventing (0,0) spawning

1) Add necessary additions to the following code to prevent the 'wave' sound effect from playing if the bullet is is timed with does not exist.

Hit 'Show' to show possible answers.

There are multiple ways to approach this problem. One solution is to play the sound effect in BulletCommands and check if the bullet still exists before playing it, as follows:

task SpawnWave{
    //feel free to add your own variables
    ascent(i in -1..2){
        ascent(j in 0..5){
            let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 3 + j/3, GetAngleToPlayer(objBoss) + i*30, DS_BALL_BS_SKY, 0);
            let obj2 = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 3 + j/3, GetAngleToPlayer(objBoss) + i*30, DS_BALL_BS_BLUE, 0);
            BulletCommands(obj, -1);
            BulletCommands(obj2, 1);
        }
    }
}

task BulletCommands(obj, dir) {
    loop(60){yield;}
    ObjMove_SetAngularVelocity(obj, dir*4);
    if(!Obj_IsDeleted(obj)){
        PlaySE(wave); //assume wave is a variable storing the path to a sound effect
    }
}

However, this method is horribly inefficient, as it attempts to play the sound effect up to 15 times in the same frame. Another less intuitive method is to use an array to store the objects and, if there is at least one bullet in the array after 60 frames have passed, play the sound effect.

task SpawnWave{
    let bullets = [];
    ascent(i in -1..2){
        ascent(j in 0..5){
            let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 3 + j/3, GetAngleToPlayer(objBoss) + i*30, DS_BALL_BS_SKY, 0);
            let obj2 = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 3 + j/3, GetAngleToPlayer(objBoss) + i*30, DS_BALL_BS_BLUE, 0);
            BulletCommands(obj, -1);
            BulletCommands(obj2, 1);
            bullets = bullets ~ [obj];
            bullets = bullets ~ [obj2];
        }
    }
    loop(60){yield;}
    if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) <= 0){return;}
    let play = false;
    ascent(i in 0..length(bullets)){
        if(!Obj_IsDeleted(bullets[i])){ //a bullet exists
            play = true;
            break; //break out of the ascent loop
        }
    }
    if(play){
        PlaySE(wave); //assume wave is a variable storing the path to a sound effect
    }
}

task BulletCommands(obj, dir) {
    loop(60){yield;}
    ObjMove_SetAngularVelocity(obj, dir*4);
}

There are other methods that can be used to solve this problem, of course.


2) Koishi is playing with cutin functions when suddenly, the Single ends, but the text and graphics from the cutin persist into the next nonspell. What is the problem, and how can she solve it?

SetAutoDeleteObject was either not set or was set to false, and the cutin function did not handle deletion of the cutin graphics and text. SetAutoDeleteObject should be set to true in the script for a local but adaptable solution, or the cutin should handle deletion for a global but specific solution.

Summary

  • Make sure that objects are never created at the location of a deleted object
  • Delete your unneeded objects at the end of a script
  • Avoid spawning bullets from nonexistent or deleted enemies
  • Terminate tasks early using return;

Sources and External Resources

N/A