Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 8 - Introduction to Shotsheets and Bullet Properties

The video for this lesson is Infinite Ultima Wave's Christmas Letty script, one of the scripts I remember very fondly. It's quite good and despite all the red and yellow bullets, the bullet graphics chosen seem to fit Letty very well.

Part 1: What will be Covered in this Lesson?

In this lesson, I will introduce shotsheets as well as blend types, and will explain the properties of bullets as well as the ObjMove and ObjRender functions that can be used with them

Although seemingly disjoint from the rest of the tutorials, I will be providing basic information on graphics in Danmakufu, and will also explain how I (and quite a few other) danmakufu scripters choose to load our shotsheets.

Part 2: What are Shotsheets and Shot Constant Sheets?

By now, you should be familiar with the presence of #include "script/default_system/Default_ShotConst.txt" at the top of our Single scripts. If you remove this while firing shots in the script, you will get errors stating that some final constant in all caps has not been defined. Here I will provide a brief explanation for how this works, as well as shotsheets themselves.

First we will observe the Default_ShotConst.txt file in the default system, which is a shot constant file we have included. A shot constant sheet is a file that references an image containing shots (which it loads, along with an associated shotsheet), and contains a list of final constants which can be used to reference the numerical IDs of bullets defined in the shotsheet. As for the include, an include basically takes the contents of the included file and spills them into the script calling #include, specifically at the place where the #include was.

local
{
	let current = GetCurrentScriptDirectory();
	let path = current ~ "Default_ShotData.txt";
	LoadEnemyShotData(path);
}

// 粒弾 --------------------------------
let DS_BALL_SS_RED		= 1;
let DS_BALL_SS_ORANGE		= 2;
let DS_BALL_SS_YELLOW		= 3;
let DS_BALL_SS_GREEN		= 4;
let DS_BALL_SS_SKY		= 5;
let DS_BALL_SS_BLUE		= 6;

//...

If we look inside the shot constant file (sample above), it states a path to Default_ShotData.txt (the shotsheet) and then loads the enemy shot data using LoadEnemyShotData(). After this, a large number of final constants are declared, each referring to a number. These are the shot graphic IDs, and we will discuss them again later in this lesson.

If you've been peeking around in other peoples's scripts, you may have noticed that not every scripter chooses to #include a shot constant sheet, instead loading the shotsheet and texture inside a single and using the actual graphic ID numbers defined in the shotsheet. There is little to no benefit to doing this - it is a matter of style. However, some shot constant sheets do not account for every bullet graphic present in the shotsheet, and scripters using shot constant sheets may never use certain bullet graphics because no final constant exists for those particular bullet ids. You can still refer to the bullets by number as well as by their final constant names, but most scripters do not mix and match final constants and numbers for shot graphics, instead sticking to one or the other. It's up to you whether to use a shot constant sheet or not - the benefit is that if you change shotsheets while a script is under development, it is possible that the constant names will be the same between different shot constant sheets. However, there is no universal standard for shot constant names, especially with new bullet types. Below is an example for how you might manually load a shotsheet if you decide to not #include a shot constant sheet.

    let ZUNbullet = GetCurrentScriptDirectory() ~ "./AllStarShot.dnh";

@Initialize{
    //...
    LoadTexture(ZUNbullet);
    LoadEnemyShotData(ZUNbullet);
    //...
}

The file we will now observe is Default_ShotData.txt in the default system. Inside this file is something that looks completely different to the code we have been writing. What is going on here? No semicolons? This is a shotsheet. To begin a shotsheet, #UserShotData is used, and shot_image points to the path of the image file containing the bullet graphics. shot_image is NOT a variable. delay_rect is the part of the image that contains the delay cloud for the bullet.

Below is a sample from the default shotsheet. Here, the graphic with id = 1 corresponds to DS_BALL_SS_RED. If you were to include the shot constant sheet or manually load the shotsheet, you could use either 1 or DS_BALL_SS_RED in order to use the red ball graphic.

#UserShotData

shot_image = "./img/Default_Shot.png"
delay_rect = (209, 474, 240, 505)


// 粒弾 --------------------------------
ShotData{
	id = 1
	rect = ( 0, 0, 12, 12 )
	delay_color = ( 255, 63, 63 )
}
ShotData{
	id = 2
	rect = ( 12, 0, 24, 12 )
	delay_color = ( 255, 127, 63 )
}

//...

Of particular note are the four parameters for delay_rect. They are left, top, right, and bottom, and refer to the pixels on the image file, and this left-top-right-bottom (LTRB) system is followed throughout all of Danmakufu. The numbers form a rectangle around the graphic, and 0 refers to the left or top of the image for x values (left, right) and y values (top, bottom) respectively.

Now we will discuss the properties that can be assigned to shots in a shotsheet.

EXERCISE: Go back to the code we have written in Lesson 7. Change bullet graphics, switch between constants and numbers, and try loading the shotsheet manually rather than including a shot constant sheet. Find a style that best suits you.

Part 3: What are the Properties of a Shot in a Shotsheet?

Inside ShotData{} are a number of properties that can be assigned to shots. Of particular note are id, rect, and delay_color, which are required for proper functioning of the shot

id is the number that distinguishes the bullet graphic. It is what is referenced by the graphic parameter of most shot creation functions, and is also referenced in the shot constant sheet. It should be unique for the bullet.

rect, like in the delay_rect, determines the location of the shot on the image referenced in shot_image and controls what the shot will look like.

Finally, delay_color is the color of the delay graphic, taken in (R, G, B) format where each color component is on a 0-255 scale.

Besides the three properties listed above, you can also use render, angular_velocity, fixed_angle, and collision. You can also have Animation Data for animated shots, although this will not be discussed in this tutorial.

Now to discuss the other properties. Firstly, render. This sets the Blend Type. The default is ALPHA, which renders the graphic as-is, including transparency from the Alpha channel. You can also do ADD and ADD_ARGB, which makes the bullet shiny. ADD requires a black background to make things shiny - otherwise it looks hideous. ADD_ARGB is based on the Alpha channel instead, and makes partially transparent bullets shiny. You can also use Subtractive and Multiplicative blends with SUBTRACT and MULTIPLY, but these are not usually used. Most of the time, you will use a function that will be discussed in Lesson 14, ObjRender_SetBlendType(), to change the render type of the graphic, and shotsheets therefore tend to contain only ALPHA and either ADD or ADD_ARGB shots. If render is not filled out, it defaults to ALPHA.

Next is angular_velocity. This controls the rotation of the bullet graphic. Default is 0, for no rotation.

Next is fixed_angle, which takes a boolean. True means that the bullet will always take the shape in the shotsheet and will never rotate. Default is false.

Finally, we have collision, which is the hitbox. By default, the hitbox is a percentage of the bullet graphic (in a circle, specifically max(min(width, height) / 3 - 3, 3)), but by setting a value to collision (in pixels), you can specify a different radius for the hitbox, which originates from the center of the LTRB rectangle. It is also possible to set collision at an offset from the center of the bullet using collision = (r, x, y).

Below are more examples of shot definitions.

ShotData{ id=335 render=ALPHA  delay_color= (64,255,255) 
	AnimationData{ 
		animation_data=(4,0,448,32,488)
		animation_data=(4,32,448,64,488)
		animation_data=(4,64,448,96,488)
		animation_data=(4,96,448,128,488)
	}
	collision = 5;
}
ShotData{ id=847 rect=(448,256,479,287) render=ADD_ARGB collision=4 delay_color= (255,255,255) } // Gray

ShotData{ id=855 rect=(768,127,831,190) render=ADD_ARGB fixed_angle=true collision = 13 delay_color= (255,255,255) } // Gray

With that out of the way, let's begin our discussion on actual shots and their properties.

CHECKPOINT: What properties are required to define a shot in a shotsheet?
EXERCISE: Look at a shotsheet and examine the rects. Look at the image stated in shot_image. See how the rects correspond to the pixels of the image.

Part 4: What are the Properties of a Bullet? (ObjMove)

Touhou Danmakufu ph3, unlike its predecessor 0.12m, is similar to an object oriented scripting language. You may have noticed a lot of Obj<Type> functions, and this is in part due to the nature of ph3. While not a true object oriented language, it shares many of the characteristics of one, including inheritance. We will have a long and lengthy discussion about this later on in this tutorial, but for now, I will try to make it as simple as possible.

Earlier we discussed the properties of the bullet graphic. Now we will discuss the properties of the bullet itself. Remember CreateShotA1()? It had a number of arguments, such as its x and y location, its angle, speed, graphic (as discussed earlier), and delay time. These are properties of the bullet that are set when the object is created (all of the ones previously mentioned default to 0). Additionally, CreateShotA2() allows you to set acceleration and maximum/minimum speed.

Let's say that you want to create a shot starting at the boss that accelerates at the player from a speed of 3 pixels/frame to 6 pixels/frame. To do this, you would do the following:

CreateShotA2(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 3, GetAngleToPlayer(objBoss), 0.05, 6, 5, 0);

To best explain acceleration and maximum/minimum speed, it is important to note that without a max/min speed set prior, acceleration will not occur at all. When creating shots that take acceleration and min/max speed as parameters, this is not a problem, but it may be a problem later on. Additionally, the min/max speed is a minimum or a maximum depending on the initial speed, as well as the direction of the acceleration (whether it is positive or negative). If you have a starting speed of 5 and a min/max of 0, and your acceleration is negative, the bullet will start at a speed of 5 and decrease to 0, which is normal behavior - Danmakufu treats the min/max speed as a minimum. However, if the acceleration is positive, the bullet will not move at all and will have a speed of 0 to begin with. Take note, because in this case Danmakufu is treating the min/max speed as a maximum. Note that Danmakufu adds/subtracts speed based on acceleration each frame, and uses the max speed as a clamp - if acceleration is 0.5, max speed is 5, and current speed is 4.75, the next frame speed will be clamped to 5 and will not be 5.25.

Besides CreateShotA1() and CreateShotA2(), there are also CreateShotB1() and CreateShotB2(), which use x and y velocities rather than angle and speed. If you want gravity bullets, you may end up using these. Do be aware, however - you cannot apply any angle/speed changing functions to these and vice versa.

Now it is time to take a look at the Move Object Functions.

Using the ObjMove_Set<Field>() functions, you can set the x, y, position (which is both x and y at once), speed, angle, acceleration, and max/min speed. You can also use the movement functions we used for the boss, as well as set angular velocity.

Angular velocity changes the angle of the bullet. The angle is incremented by the amount specified in the function every frame. However, how do we use ObjMove_SetAngularVelocity()? Or any of the functions above that take an Object ID as a parameter (such as ObjMove_SetPosition, such as the AddPattern functions, which we will soon discuss?

CHECKPOINT: How quickly does speed change with acceleration? How quickly does it reach its min/max value?

Part 5: How Do I Control a Bullet?

Bullet control in ph3 is significantly easier than in 0.12m. You don't need to create an object bullet, per se. Instead, you can do this!

let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, GetAngleToPlayer(objBoss), 5, 10);
ObjMove_SetAngularVelocity(obj, 0.1);
BulletCommands(obj);

//...

task BulletCommands(obj){
    wait(60);
    ObjMove_SetAngularVelocity(obj, 0);
}

The portion before the //... is how to obtain the Object ID of a bullet, and then perform a function with that ID as an input parameter. After this, I call BulletCommands(obj), a task that I have created that will reset the angular velocity of the bullet to 0 after 60 frames. Using tasks this way is a powerful tool that, when used in conjunction with the AddPattern functions, can allow you to do all kinds of things with bullets, giving you immense control over what a bullet does. For example, I oftentimes have BulletCommands change the angular velocity and speed of the bullet, with while loops and wait times to denote when to actually begin execution. For example:

task BulletCommands(obj){
    while(ObjMove_GetSpeed(obj) > 0){yield;}
    wait(30);
    let angleT = rand(0, 360);
    loop(12){
        CreateShotA2(ObjMove_GetX(obj), ObjMove_GetY(obj), 0, angleT, 3, 0);
        angleT += 360/12;
    }
    Obj_Delete(obj);
}

The above code waits for the bullet to have a speed of 0 (assume that it was accelerating in the negative direction beforehand), then waits 30 frames before spawning a ring of bullets at the bullet's location and deleting the bullet, giving the appearance of the bullet having exploded into other bullets.

To provide another example, let's say we want to spawn a ring of bullets that aims at the player after 30 frames. First, we create the bullets:

let angle = rand(0, 360);
loop(15){
    let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, angle, 1, 0);
    BulletCommands(obj);
}

Inside our BulletCommands task, we will first want to wait 30 frames. After that, the bullet will change direction.

task BulletCommands(obj){
    wait(30);
    ObjMove_SetAngle(obj, GetAngleToPlayer(obj));
}

And that's that. Note that although getting bullets to change properties or do something after a certain number of frames is difficult without a task to handle it, it is possible without a separate task if you use ObjMove_AddPattern. Of course, ObjMove_AddPattern allows for you to do so much more as long as you know exactly when the bullet's properties should change.

There are 7 ObjMove_AddPattern functions - 4 for angle/speed bullets and 3 for xspeed/yspeed bullets. Remember that you cannot use angle/speed functions on xspeed/yspeed bullets, and vice versa. Remember when I had that wait(30); in the code above? Well, the 'frame' argument does the same thing, except that the function presets the behavior of the bullet. When the allotted frames have passed, the bullet's properties will change to match those assigned in ObjMove_AddPattern when the function was called. In other words, if you set angle to be GetAngleToPlayer(objBoss) in ObjMove_AddPattern at 60 frames, the angle when the frames have passed will be the angle from boss to player 60 frames ago, not the current angle to the player. Alternatively, you could use the BulletCommands method, where you have the actual angle to the player. However, firing at where the player used to be also has its uses, so keep that in mind - ObjMove_AddPattern is a powerful and useful tool. If you do not want to change one of the options, use NO_CHANGE for the value - this is a constant that will tell Danmakufu to not change the current value.

However, do be aware that ObjMove_AddPattern has the tendency to do weird and unexpected things that you don't expect, especially in regards to the acceleration and max/min speeds. Additionally, be warned that if there is no acceleration and there is angular velocity, the bullet will form a circle and return to its spawning location. This can cheapshot players, break patterns, or be an amazing tool for beautiful patterns that require unique methods of dodging.

CHECKPOINT: How do you execute functions on an object using its Object ID?
EXERCISE: Use ObjMove_AddPattern functions in your own script. Experiment with it and see what you can do. I suggest using rings of bullets, as the changes are more apparent that way.

Part 6: What are the Properties of a Bullet? (ObjShot, ObjRender)

Of course, ObjMove commands are not the only functions that can be used on bullets. Shot objects not only inherit all move and render functions - they have functions of their own.

Of these, there are a two that really stand out: ObjShot_SetAutoDelete(obj) and ObjShot_SetSpellResist(obj)

The former, ObjShot_SetAutoDelete(obj), controls auto-deletion of a bullet when the bullet goes past a certain boundary offscreen (out of the shot autodelete clip, to be more accurate). The latter, ObjShot_SetSpellResist(obj), controls whether or not a shot will be deleted when the player bombs and/or when the player dies. By using these two functions, it is possible to control when a bullet will be deleted, etc. Very useful.

    let obj = CreateShotA1(ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 2, GetAngleToPlayer(objBoss), 1, 5);
    ObjShot_SetAutoDelete(obj, false); //Bullet will not be autodeleted past the shot autodelete clip
    ObjShot_SetSpellResist(obj, true); //Bullet will not be deleted by bombs or player death
    wait(360);
    Obj_Delete(obj);//Since the bullet would not be deleted until the end of the script, 
    //it is important to delete the bullet manually if it is not meant to stay on-screen

Something to note: not all of the functions under ObjShot can be used by enemy bullets - these functions are sometimes meant for use only by player shots, such as ObjShot_SetPenetration(obj). Be aware of this - all damage, penetration, erase shot, spell factor, etc. functions are only for player shots.

Now I will discuss ObjRender functions.

ObjRender functions can be used on any object that has a graphic, whether it be a shot, enemy, or a background image. For the purposes of this lesson, I will only cover the ways to manipulate the graphic of a shot bullet.

I will start with the SetX, SetY, SetZ, and SetPosition functions. The first thing to note is that these set the location of the graphic. However, when you change the location of the graphic, the actual object does not necessarily move along with it - for some object types, the graphics can be moved independently of the actual object position. Be aware of this. Generally speaking, you should never use these movement-control functions on shots. Note: ObjRender_SetX/Y behaves the same way as ObjMove_SetX/Y on shots (Tested June 19, 2018).

Angle and Scale, however, are useful. Want to make your bullet graphic spin but move in the same direction? Want to make your bullet change size? This is how you do it. However, keep in mind that if you change the scale of your bullet, the size of the hitbox does not automatically change with it - you will have to take care of that separately.

There is no function to make an object's graphic rotate (IE there is no ObjRender_SetAngularVelocity). You will have to set angular_velocity in the shotsheet or place ObjRender_SetAngleZ(obj) in a while loop. Additionally, you will most likely not be using SetAngleX, SetAngleY, or SetAngleXYZ for bullets - there simply isn't much of a need to use them. As for scale, you can throw SetScaleX in a while loop and change it based on a sine wave to get the effect that Yuke had in Unreasonable Mechanism (which I duplicated for use in some of my scripts). Experiment with SetScale as much as you want, but always remember that the size of the bullet's hitbox does not change with the scale, so be wary when setting the scale to something smaller than the hitbox of the bullet.

ObjRender_SetColor(obj, r, g, b) and ObjRender_SetAlpha(obj, a) are two of the most amazing functions ever to exist. In 0.12m, they existed as a single function that was a pain to write out, but in ph3, you can use them to change the color and alpha (transparency) of an object. By default, ObjRender_SetColor(obj, 255, 255, 255); is the graphic as-is, but you can darken it/do stuff with it. It's really helpful for auras and the like, because when you have a white graphic, any ObjRender_SetColor(obj, r, g, b); you apply to it will change the color of the graphic when displayed to the given rgb values. As for Alpha, the default is 255, and by lowering it, you can change the transparency of the graphic. Really amazing and helpful for non-damaging bullets and graphical effects.

ObjRender_SetBlendType() is the last function I will discuss in this lesson. As stated earlier, you can use this function to set the blend type of an object (See Part 3 for the blend types). Be aware that your processor may have a hard time if you abuse this, and excessive use of ADD rendering may be hard on the eyes. Use ADD blending with dark backgrounds for good auras and contrast.

CHECKPOINT: How do you rotate a shot graphic using shotsheets? Using ObjRender_SetAngle()?

Part 7: What is the Difference Between ObjMove and ObjRender?

To close this lesson, I will provide a brief but hopefully informative guide to the differences between ObjMove and ObjRender. For reference, we will be using this image, which is from the official documentation for Touhou Danmakufu ph3. This image shows object inheritance in Touhou Danmakufu ph3. For example, ObjEnemy can use ObjSprite2D, ObjPrim, ObjRender, ObjShader, Obj, and ObjMove functions, because it is a subclass of all of those. By this same logic, only ObjEnemy, ObjShot and its subclasses, and ObjItem can use ObjMove functions. This is very important, because if you try to use ObjMove functions on a spell object (there is a reason why you should not begin Danmakufu by making a player script), you will find that it does not work.

The chart is self-explanatory, but remember that for shots, enemies, and items, ObjRender functions control the graphic and ObjMove functions control the actual object.

What you do with the functions is, in the end, up to you. But use them in a proper manner.

EXERCISE: Use ObjRender_SetColor() and ObjRender_SetAlpha() on some bullets after a set time. See what effect it has on the bullets.

Quiz: Bullet Properties

1) In a shotsheet, a bullet graphic is 16x16 pixels. Later on, ObjRender_SetScaleXYZ(obj, 0.5, 0.5, 1) is called on the bullet. What should be done to make the hitbox of the bullet fair?

A. Change the size of the hitbox in the shotsheet
B. Change the size of the hitbox in the script
C. Don't change anything.

2) Keine is trying to make a bullet change its movement angle after 60 frames. How can she do this? (there are multiple correct answers)

A. ObjMove_AddPatternA1()
B. task BulletCommands(obj, n){wait(60); ObjRender_SetAngleZ(obj, n);}
C. ObjMove_SetDestAtFrame()
D. task BulletCommands(obj, n){wait(60); ObjMove_SetAngle(obj, n);}

3) True or False? You can use ObjMove functions on Spell Objects.

A. true
B. false

4) Which of the following controls deletion of bullets when the player dies?

A. ObjShot_SetSpellResist()
B. ObjShot_SetAutoDelete()
C. Obj_SetVisible()

5) True or False? ALPHA is the default Blend Type.

A. true
B. false

6) Write code to rotate a shot graphic such that it makes a full rotation after 37 frames.

Hit 'Show' to show possible answers.

One way of doing this is to reset the angle each frame, adding 360/37 (the amount to rotate each frame) to the current angle.

task RotateShot(obj){
    while(!Obj_IsDeleted(obj)){
        ObjRender_SetAngleZ(obj, ObjRender_GetAngleZ(obj) + 360/37);
        yield;
    }
}

Another approach uses a counter and does not require obtaining the original value. This code assumes that the starting AngleZ was 0.

task RotateShot(obj){
    let objcount = 0;
    while(!Obj_IsDeleted(obj)){
        ObjRender_SetAngleZ(obj, 360/37 * objcount);
        objcount++;
        yield;
    }
}

There are other ways to do this, of course, but above are two possible ways. You will need to adapt your code for each given situation.

Summary

  • Shot constant sheets allow scripters to use descriptive constant names when referring to shot graphic IDs
  • Shotsheets contain a number of different options for setting bullet properties
  • By passing an Object ID, it is possible to use Obj<Type> functions to change a bullet's properties
  • You can pass an Object ID into a task to timed commands on an object
  • You can use ObjMove_AddPattern functions to make a shot change its properties a certain number of frames after spawning

Sources and External Resources

N/A