Site Logo


Sparen's Danmakufu ph3 Tutorials Extra Lesson 5 - Libraries: Circular Lifebar

Part 1: What will be Covered in this Lesson?

In this lesson, I will discuss the existing boss lifebar as well as how it can be adapted into a circular lifebar. Please note that there are multiple ways to do circular lifebars and that this is just one method.

This lesson requires understanding of 2D primitives in Danmakufu - especially Triangle Strip. If you are not familiar with Triangle Strips, Sprite Lists and other 2D primitive functions, please refer to Lesson 29.

For this lesson, please prepare an 'empty' single script and a plural script that calls the Single script. These can be pretty barebones since they just need to show differing HP amounts. I'll post samples below.

For the Single, make two variations - one with 500 HP and one with 1000 HP.

//#TouhouDanmakufu[Single]
#ScriptVersion[3]
#Title["U3L29A Circular Lifebar"]
#System["U3L29A.dnh"]

let objBoss;

@Event{
    alternative(GetEventType())
    case(EV_REQUEST_LIFE){
        SetScriptResult(500);
    }
    case(EV_REQUEST_TIMER){
        SetScriptResult(120);
    }
}

@Initialize{
    objBoss = ObjEnemy_Create(OBJ_ENEMY_BOSS);
    ObjEnemy_Regist(objBoss);
    ObjMove_SetPosition(objBoss, 192, 64);
}

@MainLoop{
    ObjEnemy_SetIntersectionCircleToShot(objBoss, ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 32); 

    if(ObjEnemy_GetInfo(objBoss, INFO_LIFE) <= 0) {
        Obj_Delete(objBoss);
        CloseScript(GetOwnScriptID());
    }
    yield;
}

For the Plural, we'll test out three different combinations of the two Singles.

#TouhouDanmakufu[Plural]
#ScriptVersion[3]
#Title["U3L29A Circular Lifebar Plural"]
#System["U3L29A.dnh"]

let objBoss;

@Initialize{
    TPlural;
}

@MainLoop{
    yield;
}

task TPlural{
    let dir=GetCurrentScriptDirectory();
    let obj=ObjEnemyBossScene_Create();
    ObjEnemyBossScene_Add(obj,0,dir~"./Unit 3 Lesson 29A-CLB SingleA.dnh");
    ObjEnemyBossScene_Add(obj,1,dir~"./Unit 3 Lesson 29A-CLB SingleA.dnh");
    ObjEnemyBossScene_Add(obj,1,dir~"./Unit 3 Lesson 29A-CLB SingleB.dnh");
    ObjEnemyBossScene_Add(obj,2,dir~"./Unit 3 Lesson 29A-CLB SingleA.dnh");
    ObjEnemyBossScene_Add(obj,2,dir~"./Unit 3 Lesson 29A-CLB SingleA.dnh");
    ObjEnemyBossScene_Add(obj,2,dir~"./Unit 3 Lesson 29A-CLB SingleB.dnh");
    ObjEnemyBossScene_LoadInThread(obj);
    ObjEnemyBossScene_Regist(obj);
    while(!Obj_IsDeleted(obj)){
        yield;
    }
    CloseScript(GetOwnScriptID());
}

Note the #System file - the entirety of the work we do in this tutorial will occur in a system script - I suggest copying the default one for now.

Part 2: How does the basic lifebar work?

First, let's start with the basics - what exactly is a lifebar? Well, to put it simply, it's the visual indicator of how much HP the boss has and is usually depicted with a bar or circle that starts full and begins to empty out over time.

With regards to linear vs. circular, it's really just a matter of style - linear is more efficient but some like the circular lifebar since it's easier to see due to its position relative to the boss.

Let's first dive into the default lifebar code (note that I've adjusted filepaths).

task TBossLife
{
	let path = GetCurrentScriptDirectory() ~ "img-clb/Default_System.png";
	let obj = ObjPrim_Create(OBJ_SPRITE_LIST_2D);
	ObjPrim_SetTexture(obj, path);
	Obj_SetRenderPriority(obj, 0.7);

	let lastRemStep = -1;
	let lifeRateRender = 0;

	let objScene = ID_INVALID;
	loop
	{
		objScene = GetEnemyBossSceneObjectID();
		ObjSpriteList2D_ClearVertexCount(obj);
		if(objScene != ID_INVALID)
		{
			RenderLife();
		}
		yield;
	}


	function RenderLife()
	{
		let countRemStep = ObjEnemyBossScene_GetInfo(objScene, INFO_REMAIN_STEP_COUNT);
		if(lastRemStep != countRemStep)
		{
			lifeRateRender = 0;
		}

		let lifeTotalMax = ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_TOTAL_MAX_LIFE);
		let lifeTotal = ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_TOTAL_LIFE);
		let lifeRate = min(lifeTotal / lifeTotalMax, lifeRateRender);
		ObjSpriteList2D_SetSourceRect(obj, 1, 1, 127, 11);
		ObjSpriteList2D_SetDestRect(obj, 72, 8, 72 + 270 * lifeRate, 12);
		ObjSpriteList2D_AddVertex(obj);

		ObjSpriteList2D_SetSourceRect(obj, 132, 1, 137, 11);
		let listLifeDiv = [0] ~ ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_LIFE_RATE_LIST);
		ascent(iDiv in 0 .. length(listLifeDiv))
		{
			let rate = listLifeDiv[iDiv];
			let x = 72 + 270 * (1-rate);
			ObjSpriteList2D_SetDestRect(obj, x-1, 4, x + 1, 14);
			ObjSpriteList2D_AddVertex(obj);
		}

		ObjSpriteList2D_SetSourceRect(obj, 1, 1, 127, 11);
		ascent(iStep in 0 .. countRemStep)
		{
			let remStepRate = 58 / countRemStep;
			ObjSpriteList2D_SetDestRect(obj, 4 + iStep * remStepRate + 2, 8,
				4 + (iStep + 1) * remStepRate, 12);
			ObjSpriteList2D_AddVertex(obj);
		}

		lifeRateRender += 0.01;
		lifeRateRender = min(lifeRateRender, 1);
		lastRemStep = countRemStep;
	}
}

Pretty hefty, huh. But it's not too bad once we break it down. The code itself is broken up into a nested function RenderLife() and the code that calls this function. The main purpose of the outer code is to set the texture for the lifebar image and create the sprite list, store some variables to be used later on, and check in a constant loop if there is a boss scene - the lifebar only renders if there's a boss, after all.

RenderLife() only runs when a boss scene is active. Note that it will run every frame when a boss scene is active. Let's walk through this line by line. See ObjEnemyBossScene_GetInfo()'s docs for some quick reference, but I'll briefly note the components used here.

  • INFO_REMAIN_STEP_COUNT: Returns number of steps (lifebars) left in current boss scene
  • INFO_ACTIVE_STEP_TOTAL_MAX_LIFE: Returns initial life set in EV_REQUEST_LIFE for the current lifebar
  • INFO_ACTIVE_STEP_TOTAL_LIFE: Returns the current life remaining in the current lifebar
  • INFO_ACTIVE_STEP_LIFE_RATE_LIST: Returns the proportion of life for the Singles in the current lifebar

As you can see from the above, Danmakufu's lifebar code operates on 'steps' and only knows about the current active step. First, RenderLife calls ObjEnemyBossScene_GetInfo(objScene, INFO_REMAIN_STEP_COUNT);, which gets the number of remaining lifebars. This is what appears in the top left as small bars, or stars if a scripter decides to use stars to indicate lifebars instead.

After checking if there are any steps left, RenderLife does a brief check used for animations (see end of this section) and then obtains the max life and the current life, then calculates the percentage to display, storing it in lifeRate. For example, let's say that we have two Singles - one with 1000 HP and one with 500 HP. If the player shoots down 500 off of the first, then the lifebar will be 2/3 full.

The main lifebar is drawn, with its dest rect showing that it starts from 72 pixels off the left of the playing field and extends up to 270 pixels, which is its total length. ObjSpriteList2D_SetDestRect works well here as the image is uniform horizontally.

After adding the main lifebar, the source rect shifts to the dividers. The listLifeDiv starts out with 0 and then concatenates the rate list to obtain the relative locations on the lifebar to insert the dividers. As noted before, the length of the lifebar is 270 pixels, and for each break between Singles, including the start at 0, the divider is added as another Sprite in the Sprite List.

Once the dividers have been added, for every remaining lifebar, the location and length (dependent on how many there are) is determined and the segments are added.

At the very end of RenderLife, lifeRateRender is incremented. This provides the animation of the lifebar 'filling up' at the start of each new lifebar. This is capped at 1 since it's a rate from 0 to 1. It is for this reason that the check at the start of RenderLife is run - when a new lifebar starts, the animation must start from 0 again.

Part 3: How do I make a circular lifebar?

We will now begin changing the code. First, please note that we will be ignoring the display for the number of remaining lifebars as the way this is handled is not integral to the circular lifebar and is often implemented differently depending on the scripter's UI design preferences.

We are left with the lifebar itself as well as the dividers. We will keep the dividers as a Sprite List but will use a Linestrip Primitive for the lifebar itself.

We'll start with the lifebar. The first major change is that we need to determine how many vertices to use. Naturally, more vertices means that we'll get closer to a circle. However, more vertices also means that this expensive operation will put more of a burden on the computer of the person playing your script, causing potential lag. For now, we'll divide the lifebar into 32 chunks, which requires 66 vertices.

Wait, what? 66 vertices? Where did THAT come from?! Well, think about what we are doing - we want to imitate a circle. However, our lifebar isn't just a line - we're wrapping a rectangular structure, so for every point where it turns, we need a PAIR of vertices.

EXAMPLE - Lifebar Image v0 v1 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15 v16 v17 EXAMPLE - Fitted Lifebar v0,16 v1,17 v2 v3 v4 v5 v6 v7 v8 v9 v10 v11 v12 v13 v14 v15

In the above example, we divide our source image as such, and then wrap it into a circle. The two edges connect, so we need those vertices to be duplicated. In addition, we want depth, so every location where there is an angle change requires two vertices. This is the same method as with Magic Circles - we will utilize the magic circle code from Lesson 29 in order to pull off the lifebar. For now, here's what we have, though it's effectively just a magic circle. I haven't moved the Single dividers yet - we'll handle that later.

task TBossLife {
    let path = GetCurrentScriptDirectory() ~ "img-clb/Default_System.png";
    let obj = ObjPrim_Create(OBJ_PRIMITIVE_2D);
    ObjPrim_SetPrimitiveType(obj, PRIMITIVE_TRIANGLESTRIP);
    ObjPrim_SetTexture(obj, path);
    Obj_SetRenderPriority(obj, 0.7);

    let NUM_VERTEX = 66; // Number of vertices we will use for this lifebar. 
    let LB_RADIUS = 96; // Maximum radius of our lifebar, in pixels
    let LB_WIDTH = 4; // Width of our lifebar, in pixels

    ObjPrim_SetVertexCount(obj, NUM_VERTEX);

    let objDiv = ObjPrim_Create(OBJ_SPRITE_LIST_2D);
    ObjPrim_SetTexture(objDiv, path);
    Obj_SetRenderPriority(objDiv, 0.7);
    let objRemLB = ObjPrim_Create(OBJ_SPRITE_LIST_2D);
    ObjPrim_SetTexture(objRemLB, path);
    Obj_SetRenderPriority(objRemLB, 0.7);

    let lastRemStep = -1;
    let lifeRateRender = 0;

    let objScene = ID_INVALID;
    loop {
        objScene = GetEnemyBossSceneObjectID();
        ObjSpriteList2D_ClearVertexCount(objDiv);
        ObjSpriteList2D_ClearVertexCount(objRemLB);
        if(objScene != ID_INVALID) {
            RenderLife();
            RenderRemainingLifebars();
        }
        yield;
    }

    function RenderLife() {
        // Information on boss. If no boss, don't run the code!
        let objBosses = GetEnemyBossObjectID();
        let objBoss = ID_INVALID;
        if (length(objBosses) == 0) {return;} // No bosses in scene; don't adjust rendering
        else {objBoss = objBosses[0];}

        // Remaining steps
        let countRemStep = ObjEnemyBossScene_GetInfo(objScene, INFO_REMAIN_STEP_COUNT);
        if(lastRemStep != countRemStep) {
            lifeRateRender = 0;
        }

        let lifeTotalMax = ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_TOTAL_MAX_LIFE);
        let lifeTotal = ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_TOTAL_LIFE);
        let lifeRate = min(lifeTotal / lifeTotalMax, lifeRateRender);
        // Set the Texture Coordinates
        ascent(iVert in 0..NUM_VERTEX / 2) {
            let left = 1 + iVert * 126/NUM_VERTEX; // 128 is the length of the default lifebar image, but we want to not use the edges
            let indexVert = iVert * 2; // indexVert refers to a PAIR of vertices
            // Even vertices are 'outer' and Odd vertices are 'inner' in this case.
            ObjPrim_SetVertexUVT(obj, indexVert + 0, left, 1);
            ObjPrim_SetVertexUVT(obj, indexVert + 1, left, 11);
        }

        // Lifebar centered around boss. Update position
        ObjRender_SetPosition(obj, ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 1);

        // Set the locations of the vertices
        ascent(iVert in 0..NUM_VERTEX / 2) {
            // Set vertex positions for current iteration of loop
            let indexVert = iVert * 2;
            // The '- 1' for the angle is since we must duplicate a pair of vertices. 
            // So if we have 34 vertices, only 16 edges will be formed.
            let angle = 360 / (NUM_VERTEX / 2 - 1) * iVert;
            // listRadius stores the current distance from the position of the render object to the position of each vertex. 
            let vx1 = (LB_RADIUS + LB_WIDTH/2) * cos(angle);
            let vy1 = (LB_RADIUS + LB_WIDTH/2) * sin(angle);
            ObjPrim_SetVertexPosition(obj, indexVert + 0, vx1, vy1, 0);
            let vx2 = (LB_RADIUS - LB_WIDTH/2) * cos(angle);
            let vy2 = (LB_RADIUS - LB_WIDTH/2) * sin(angle);
            ObjPrim_SetVertexPosition(obj, indexVert + 1, vx2, vy2, 0);
        }

        ObjSpriteList2D_SetSourceRect(objDiv, 132, 1, 137, 11);
        let listLifeDiv = [0] ~ ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_LIFE_RATE_LIST);
        ascent(iDiv in 0 .. length(listLifeDiv)) {
            let rate = listLifeDiv[iDiv];
            let x = 72 + 270 * (1-rate);
            ObjSpriteList2D_SetDestRect(objDiv, x-1, 4, x + 1, 14);
            ObjSpriteList2D_AddVertex(objDiv);
        }

        lifeRateRender += 0.01;
        lifeRateRender = min(lifeRateRender, 1);
        lastRemStep = countRemStep;
    }

    function RenderRemainingLifebars() {
        let countRemStep = ObjEnemyBossScene_GetInfo(objScene, INFO_REMAIN_STEP_COUNT);
        ObjSpriteList2D_SetSourceRect(objRemLB, 1, 1, 127, 11);
        ascent(iStep in 0 .. countRemStep) {
            let remStepRate = 58 / countRemStep;
            ObjSpriteList2D_SetDestRect(objRemLB, 4 + iStep * remStepRate + 2, 8, 4 + (iStep + 1) * remStepRate, 12);
            ObjSpriteList2D_AddVertex(objRemLB);
        }
    }
}

Some things to note about the above: First, we're not animating the lifebar yet, and the lifebar does not expand like the magic circle, so that code has been removed. Second, we aren't using the entire lifebar image, similar to the default. This avoids the 'endings' looking ugly due to blur.

Now, the key difference of course is that a lifebar doesn't always show its full length. And that is where the code required for a magic circle diverges further from that of a lifebar. We already know that lifeRate represents, on a 0 to 1 scale, how much of the lifebar is filled. We can apply the same to vertex pairs, and use ObjPrim_SetVertexAlpha() to simulate the damage done. However, note that this requires a lot of vertices to be precise!

        // Set the locations of the vertices
        ascent(iVert in 0..NUM_VERTEX / 2) {
            // Set vertex positions for current iteration of loop
            let indexVert = iVert * 2;
            // The '- 1' for the angle is since we must duplicate a pair of vertices. 
            // So if we have 34 vertices, only 16 edges will be formed.
            let angle = -360 / (NUM_VERTEX / 2 - 1) * iVert;
            // listRadius stores the current distance from the position of the render object to the position of each vertex. 
            let vx1 = (LB_RADIUS + LB_WIDTH/2) * cos(angle*lifeRate);
            let vy1 = (LB_RADIUS + LB_WIDTH/2) * sin(angle*lifeRate);
            ObjPrim_SetVertexPosition(obj, indexVert + 0, vx1, vy1, 0);
            let vx2 = (LB_RADIUS - LB_WIDTH/2) * cos(angle*lifeRate);
            let vy2 = (LB_RADIUS - LB_WIDTH/2) * sin(angle*lifeRate);
            ObjPrim_SetVertexPosition(obj, indexVert + 1, vx2, vy2, 0);
        }

Note that we've attached a negative sign to the angle. This allows the animation to proceed from 'base' to 'end' of the lifebar. In order to simulate the life, we have multiplied the angle by the lifeRate, effectively 'squeezing' our lifebar into a smaller angle range in order to simulate the lifebar. As always, more vertices = more lag = better graphical performance, so feel free to adjust that to improve the animation as well.

Now we have two more things to do - first, add the Single dividers, and second, replace the graphics with graphics that are actually suitable for a circular lifebar instead of the defaults, where the gradient is suitable for a straight lifebar but not for a curved one.

        ObjSpriteList2D_SetSourceRect(objDiv, 132, 1, 137, 11);
        let listLifeDiv = [0] ~ ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_LIFE_RATE_LIST);
        ascent(iDiv in 0 .. length(listLifeDiv)) {
            let rate = listLifeDiv[iDiv];
            let targetangle = 360 * rate;
            ObjSpriteList2D_SetDestCenter(objDiv);
            ObjRender_SetAngleZ(objDiv, targetangle + 90);
            ObjRender_SetPosition(objDiv, ObjMove_GetX(objBoss) + LB_RADIUS * cos(targetangle), ObjMove_GetY(objBoss) + LB_RADIUS * sin(targetangle), 1);
            ObjSpriteList2D_AddVertex(objDiv);
        }

For the dividers, it's quite simple to adjust them. All we need to do is determine where on the circle they will be using the rate array, and then put them there. Of course we also want to rotate them so that they're perpendicular to the lifebar.

And with that, the basics are done.

Part 3a: Circular Lifebar Graphical Enhancement

In this last section, we'll do the graphic replacement mentioned prior. Here we will replace the graphics used and will add a simple border. Grab the image at u3l29asample.png

Once you've replaced the texture, we'll add the border. In this image I've put both the main lifebar and border into the area formerly occupied by the lifebar. It's white, so you can change the color of either as well.

task TBossLife {
    let path = GetCurrentScriptDirectory() ~ "img-clb/Default_System_new.png";
    let obj = ObjPrim_Create(OBJ_PRIMITIVE_2D);
    ObjPrim_SetPrimitiveType(obj, PRIMITIVE_TRIANGLESTRIP);
    ObjPrim_SetTexture(obj, path);
    Obj_SetRenderPriority(obj, 0.7);

    let objBorder = ObjPrim_Create(OBJ_PRIMITIVE_2D);
    ObjPrim_SetPrimitiveType(objBorder, PRIMITIVE_TRIANGLESTRIP);
    ObjPrim_SetTexture(objBorder, path);
    Obj_SetRenderPriority(objBorder, 0.7);

    let NUM_VERTEX = 66; // Number of vertices we will use for this lifebar. 
    let LB_RADIUS = 96; // Maximum radius of our lifebar, in pixels
    let LB_WIDTH = 4; // Width of our lifebar, in pixels
    let LB_WIDTH_BORDER = 5; // Width of our lifebar border, in pixels

    ObjPrim_SetVertexCount(obj, NUM_VERTEX);
    ObjPrim_SetVertexCount(objBorder, NUM_VERTEX);

    let objDiv = ObjPrim_Create(OBJ_SPRITE_LIST_2D);
    ObjPrim_SetTexture(objDiv, path);
    Obj_SetRenderPriority(objDiv, 0.7);
    let objRemLB = ObjPrim_Create(OBJ_SPRITE_LIST_2D);
    ObjPrim_SetTexture(objRemLB, path);
    Obj_SetRenderPriority(objRemLB, 0.7);

    let lastRemStep = -1;
    let lifeRateRender = 0;

    let objScene = ID_INVALID;
    loop {
        objScene = GetEnemyBossSceneObjectID();
        ObjSpriteList2D_ClearVertexCount(objDiv);
        ObjSpriteList2D_ClearVertexCount(objRemLB);
        if(objScene != ID_INVALID) {
            RenderLife();
            RenderRemainingLifebars();
        }
        yield;
    }

    function RenderLife() {
        // Information on boss. If no boss, don't run the code!
        let objBosses = GetEnemyBossObjectID();
        let objBoss = ID_INVALID;
        if (length(objBosses) == 0) {return;} // No bosses in scene; don't adjust rendering
        else {objBoss = objBosses[0];}

        // Remaining steps
        let countRemStep = ObjEnemyBossScene_GetInfo(objScene, INFO_REMAIN_STEP_COUNT);
        if(lastRemStep != countRemStep) {
            lifeRateRender = 0;
        }

        let lifeTotalMax = ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_TOTAL_MAX_LIFE);
        let lifeTotal = ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_TOTAL_LIFE);
        let lifeRate = min(lifeTotal / lifeTotalMax, lifeRateRender);
        // Set the Texture Coordinates
        ascent(iVert in 0..NUM_VERTEX / 2) {
            let left = 1 + iVert * 62/NUM_VERTEX; // Avoid edges
            let indexVert = iVert * 2; // indexVert refers to a PAIR of vertices
            // Even vertices are 'outer' and Odd vertices are 'inner' in this case.
            ObjPrim_SetVertexUVT(obj, indexVert + 0, left, 0);
            ObjPrim_SetVertexUVT(obj, indexVert + 1, left, 12);
            ObjPrim_SetVertexUVT(objBorder, indexVert + 0, 64 + left, 0);
            ObjPrim_SetVertexUVT(objBorder, indexVert + 1, 64 + left, 12);
        }

        // Lifebar centered around boss. Update position
        ObjRender_SetPosition(obj, ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 1);
        ObjRender_SetPosition(objBorder, ObjMove_GetX(objBoss), ObjMove_GetY(objBoss), 1);
        ObjRender_SetColor(objBorder, 255, 0, 0);
        ObjRender_SetAlpha(objBorder, 128);

        // Set the locations of the vertices
        ascent(iVert in 0..NUM_VERTEX / 2) {
            // Set vertex positions for current iteration of loop
            let indexVert = iVert * 2;
            // The '- 1' for the angle is since we must duplicate a pair of vertices. 
            // So if we have 34 vertices, only 16 edges will be formed.
            let angle = -360 / (NUM_VERTEX / 2 - 1) * iVert;
            // listRadius stores the current distance from the position of the render object to the position of each vertex. 
            let vx1 = (LB_RADIUS + LB_WIDTH/2) * cos(angle*lifeRate);
            let vy1 = (LB_RADIUS + LB_WIDTH/2) * sin(angle*lifeRate);
            ObjPrim_SetVertexPosition(obj, indexVert + 0, vx1, vy1, 0);
            let vx2 = (LB_RADIUS - LB_WIDTH/2) * cos(angle*lifeRate);
            let vy2 = (LB_RADIUS - LB_WIDTH/2) * sin(angle*lifeRate);
            ObjPrim_SetVertexPosition(obj, indexVert + 1, vx2, vy2, 0);

            let vx1b = (LB_RADIUS + LB_WIDTH_BORDER/2) * cos(angle);
            let vy1b = (LB_RADIUS + LB_WIDTH_BORDER/2) * sin(angle);
            ObjPrim_SetVertexPosition(objBorder, indexVert + 0, vx1b, vy1b, 0);
            let vx2b = (LB_RADIUS - LB_WIDTH_BORDER/2) * cos(angle);
            let vy2b = (LB_RADIUS - LB_WIDTH_BORDER/2) * sin(angle);
            ObjPrim_SetVertexPosition(objBorder, indexVert + 1, vx2b, vy2b, 0);
        }

        ObjSpriteList2D_SetSourceRect(objDiv, 132, 1, 137, 11);
        let listLifeDiv = [0] ~ ObjEnemyBossScene_GetInfo(objScene, INFO_ACTIVE_STEP_LIFE_RATE_LIST);
        ascent(iDiv in 0 .. length(listLifeDiv)) {
            let rate = listLifeDiv[iDiv];
            let targetangle = 360 * rate;
            ObjSpriteList2D_SetDestCenter(objDiv);
            ObjRender_SetAngleZ(objDiv, targetangle + 90);
            ObjRender_SetPosition(objDiv, ObjMove_GetX(objBoss) + LB_RADIUS * cos(targetangle), ObjMove_GetY(objBoss) + LB_RADIUS * sin(targetangle), 1);
            ObjSpriteList2D_AddVertex(objDiv);
        }

        lifeRateRender += 0.02;
        lifeRateRender = min(lifeRateRender, 1);
        lastRemStep = countRemStep;
    }

    function RenderRemainingLifebars() {
        let countRemStep = ObjEnemyBossScene_GetInfo(objScene, INFO_REMAIN_STEP_COUNT);
        ObjSpriteList2D_SetSourceRect(objRemLB, 1, 1, 127, 11);
        ascent(iStep in 0 .. countRemStep) {
            let remStepRate = 58 / countRemStep;
            ObjSpriteList2D_SetDestRect(objRemLB, 4 + iStep * remStepRate + 2, 8, 4 + (iStep + 1) * remStepRate, 12);
            ObjSpriteList2D_AddVertex(objRemLB);
        }
    }
}

The border maintains the same system as our original lifebar without the life adjustment for its positioning. These graphics are a quick replacement but you should find it convenient to replace and adjust the graphics to fit your needs.

Summary

  • Circular Lifebars can be implemented with TriangleStrips similar to Magic Circles
  • Shrinking the angle range available can simulate damage taken to the boss when applied to a lifebar

Sources and External Resources

N/A