Site Logo


Sparen's Danmakufu ph3 Tutorials Lesson 13 - Introduction to 2D Sprites

The video for this lesson is Koishi Adventure by Darkness1, chosen due to its usage of images and the like.

Part 1: What will be Covered in this Lesson?

We've finally arrived at the lesson you've probably all been waiting for - 2D Sprites. In this lesson, I will cover creation of 2D Sprites and how to use them for rendering basic images as well as creating a basic boss animation, among other things. So? Let's get started!

Part 2: What are 2D Sprites?

Everything in Danmakufu (and all software in general) is rendered as a primitive with a vertex shader. Primitives can be hard to understand, so we will go over more advanced primitive types in a later lesson. The good thing is that ph3 introduces 2D and 3D Sprites for rendering rectangular images, so you do not need to know the details about primitives for rendering basic images.

Let's say that you have an image that you want to display on the screen. We will render it as a 2D Sprite. Enemy Objects and rendered as 2D Sprites, so you can use 2D Sprite functions on an enemy object. For other situations, you will create a 2D Sprite using the following: ObjPrim_Create(OBJ_SPRITE_2D);. This will create a 2D Sprite, which is a rectangle in 2D space.

Part 3: What Functions Do I Use with 2D Sprites?

For the most part, you will use ObjRender and ObjSprite2D functions on 2D Sprite Objects, as well as ObjPrim_SetTexture(), render priority functions, and Blend Type functions (which will be discussed in the next lesson). First, I'll discuss ObjPrim_SetTexture().

ObjPrim_SetTexture() is the function you use to tell Danmakufu which image to render. It takes two parameters - the object ID of the primitive object, and the path to the image. It is recommended that you save the path name as a variable and then pass the variable in as the second parameter, as it looks cleaner and helps save a little computation time if you use the same image file multiple times in the same task/script. After you've set the texture, you will need to tell Danmakufu how to render the part of the image you want rendered.

2D Sprites have four vertices that form a rectangle, and you use the following three functions to tell Danmakufu which parts of the image to use and how to display the selection - ObjSprite2D_SetSourceRect(), ObjSprite2D_SetDestRect(), and ObjSprite2D_SetDestCenter().

ObjSprite2D_SetSourceRect() is the function you use to tell Danmakufu which part of the image sheet you want to render. It takes five parameters - the object to assign the rects to, and the four bounds - Left, Top, Right, and Bottom.

As a refresher, consider your image file, with (0,0) being the top left corner. In a graphical editor, use the rectangular select tool and drag from the top left (0,0) to another point (x, y). The coordinates of the destination and the origin designate the rectangle. The four bounds of this rectangle (Left, Top, Right, and Bottom) would be (0, 0, x, y).

Note that the second (top) and third (right) arguments should be one greater than displayed when you are looking at the rects in a graphical editor. In other words, if you want to use a 64x64 sprite whose bounds are (0, 0, 64, 64), you should use (0, 1, 63, 64) in the source rect function. Otherwise there may be undesirable side effects. !!!FACT CHECK NEEDED!!! **This is how it is done in the sample scripts

The source of the side effects is because of how SourceRect works - if you exceed the boundaries of an image, it will wrap. Basically, think of a 64x64 graphic. If you set the source rect to have a 64x128 image, there will be two copies of the sprite stacked vertically. The main application of this in Danmakufu is tiling seamless textures, which we will discuss along with background scripts.

Now, you have two options for the destination rects. You can manually set them using ObjSprite2D_SetDestRect(), or you can have it automatically centered, using ObjSprite2D_SetDestCenter().

What ObjSprite2D_SetDestRect() does is as follows: The position of the object acts as the origin, like with a coordinate axis. If your second (left) argument is 16, then the left side of the image will be 16 pixels to the right of the object's position. So (obj, -16, 16, 16, 16) would render the object in a square of side length 32 centered at the object's position. Basically, each argument controls the offset value of one side of the texture.

As for DestCenter, say you have a 64x96 sprite. Using ObjSprite2D_SetDestCenter(objBoss) is equivalent to using ObjSprite2D_SetDestRect(objBoss, -32, -48, 32, 48);

CHECKPOINT: How do ObjSprite2D_SetSourceRect() and ObjSprite2D_SetDestRect() work? How can you change the scale of an image by altering the Dest Rect?

Part 4: How Do I Display an Image?

It is now time to display images! For the purposes of this tutorial, I will provide a texture and a spritesheet. First, the texture - u2l13sample.png

We will begin by examining u2l13sample.png. Our mission in this part is to take this image and display it using Danmakufu. Since we have not yet covered Background scripts, simply make a task called RenderBG in a Single script and call the task in @Initialize.

Now, the components of given task. First, we will declare a variable obj and will assign it the returned object ID of ObjPrim_Create(OBJ_SPRITE_2D). This is the object we will be manipulating in this task.

The next step is ObjPrim_SetTexture(). For this tutorial, I am going to assume that you are keeping the image files in the same directory as the Single file. In practice this is poor design as it gets very hard to find things if you mix all your files - I suggest keeping all of your images in a single folder separate from your code.

There are two ways you can assign the texture to the object. You can either plug the path in to ObjPrim_SetTexture() directly, or as stated before, create a variable to contain the path. Let's use the variable, as it will make the code cleaner.

    task RenderBG{
	let obj = ObjPrim_Create(OBJ_SPRITE_2D);
	let imgpath = GetCurrentScriptDirectory() ~ "./u2l13sample.png"; //assumes image is in same directory
	ObjPrim_SetTexture(obj, imgpath);
    }

Now we need to assign the rects via ObjRender_SetSourceRect() and either ObjRender_SetDestRect() or ObjRender-SetDestCenter(). Let's open the image in a graphics editor such as Photoshop or Gimp.

The first thing you will notice is that there is a lot of transparent space. The part we want is 384x448, the size of the playing field. However, the texture is 512x512. This is because Danmakufu scales the dimensions of all textures to powers of two when loading textures, as it speeds up the process. Therefore, if you use textures whose dimensions are not powers of two, the final result will be blurred due to the scaling of the image during loading. (This is due to a DirectX9 feature - don't worry about the details). To counteract this, use images whose dimensions are powers of two. If you have a 640x512 image, for example, place it on a 1024x512 image. There will be a lot of blank space, but it will render with less blur in Danmakufu.

Anyways, we have our rects - the part we want is from (0,0) to (384,448). Let's put them in.

    task RenderBG{
        let obj = ObjPrim_Create(OBJ_SPRITE_2D);
        let imgpath = GetCurrentScriptDirectory() ~ "./u2l13sample.png";
        ObjPrim_SetTexture(obj, imgpath);
        ObjSprite2D_SetSourceRect(obj, 0, 1, 383, 448); //alternatively, 0, 0, 384, 448
        ObjSprite2D_SetDestCenter(obj);
        ObjRender_SetPosition(obj, 384 / 2, 448 / 2, 1); //move it to the center of the screen
    }

And there we have it! In regards to positioning, passing a 0 into SetDestRect will basically place that side of the image at the location of the object. In other words, if you have a 64x96 image, its position on the coordinate plane is (384/2, 448/2), and its DestRect is (0, 0, 64, 96), then the left side of the image will be on the horizontal center axis, the top side will be on the vertical center axis, and the other two sides will be offset from given axes by 64 and 96 pixels, respectively. SetDestRect can be used to scale the graphic, although ObjRender_SetScaleXYZ() is far more flexible at doing this.

Note that our current graphic is positioned and rendered at the center of the screen. If you want to lock the top left corner of the image to the top left corner of the screen, you could do the following:

    task RenderBG{
        let obj = ObjPrim_Create(OBJ_SPRITE_2D);
        let imgpath = GetCurrentScriptDirectory() ~ "./u2l13sample.png";
        ObjPrim_SetTexture(obj, imgpath);
        ObjSprite2D_SetSourceRect(obj, 0, 1, 383, 448); //alternatively, 0, 0, 384, 448
        ObjSprite2D_SetDestRect(obj, 0, 0, 384, 448);
        ObjRender_SetPosition(obj, 0, 0, 1); //move it to the top left corner of the screen
    }

As a final note, the third (z) parameter of ObjRender_SetPosition has no real usage with 2D sprites since it is completely independent from render priorities. It will be used with 3D sprites and other 3D render objects. Some people use 0 as the default, I tend to use 1. With 2D sprites, it doesn't actually matter what you use.

EXERCISE: Find a seamless texture online. Experiment with Source and Dest rects. Tile the graphic with SourceRect and scale it with DestRect. Get a feel for how you can manipulate the images.

Part 5: How Do I Animate a Boss?

Now that we have covered source rects, destination rects, and positioning, it is time for us to animate our sprites. For reference, we will use a sample spritesheet, which you can find here.

In this tutorial, I will not cover the movement or cast animations - it is better for you to figure out how to do those by yourself. However, I will be showing the idle sprites.

OK. So for the animation (the first row only), we have 4 64x64 sprites. Every 8 frames (you can change the number if you want to), we will change to the next image, and after the fourth, it will go back to the first. Let's begin coding.

Firstly, we will need to set our texture, etc.

    task TDrawLoop{
        let BossImage = GetCurrentScriptDirectory() ~ "./u2l13spritesheet.png"; //assumes that image is in same directory
        let animframe = 0;
        ObjPrim_SetTexture(objBoss, BossImage);
        ObjRender_SetScaleXYZ(objBoss, 1, 1, 1);
        ObjSprite2D_SetSourceRect(objBoss, 0, 0, 64, 64);
        ObjSprite2D_SetDestCenter(objBoss);
        while(!Obj_IsDeleted(objBoss)){

            yield;
        }
    }

In the above code, we set our texture, assign it to the boss, and do some source and dest rect manipulations. The graphic will automatically move when the boss moves because it is attached to the boss object. I also have an additional variable - animframe. We will use this to animate the boss.

First, let's consider our approach. We have four images to render, and we want each to show for 8 frames. Therefore, after 32 frames, the animation will reset. Using this, an acceptable way to animate the boss involves incrementing animframe once per frame, and then resetting it before it hits 32. But how do we get this into the rects? There are multiple ways - you can manually use if statements to set the source rects, or you can automate it, which I will show below.

Going back to Extra Lesson 1, we know that floor() will truncate positive numbers. Therefore, we can easily use this to change the source rects, since the spritesheet has been formatted in a way where there is a uniform 'grid' that controls the spacing of the sprites.

    task TDrawLoop{
        let BossImage = GetCurrentScriptDirectory() ~ "./u2l13spritesheet.png"; //assumes that image is in same directory
        let animframe = 0;
        ObjPrim_SetTexture(objBoss, BossImage);
        ObjRender_SetScaleXYZ(objBoss, 1, 1, 1);
        ObjSprite2D_SetSourceRect(objBoss, 0, 0, 64, 64);
        ObjSprite2D_SetDestCenter(objBoss);
        while(!Obj_IsDeleted(objBoss)){
            ObjSprite2D_SetSourceRect(objBoss, 0 + 64*floor(animframe/8), 0, 64 + 64*floor(animframe/8), 64);
            //We can do the above because the sprites are organized into a grid of 64x64 squares.
            animframe++;
            if(animframe >= 32){animframe = 0;}//reset if too high
            yield;
        }
    }

Above, all I did was implement the animframe changing code, as well as the resetting the source rect based on the animframe. In the current case, animframe/8 will always be between 0 and 3, which corresponds to the four sprites in the animation as they are located on the spritesheet. Using this, we can successfully render and animate the numbers 1-4.

Of course, to implement movement, etc. you would need movement angle and speed, and you can include if statements that will take care of that. Just be aware that if you decide to flip the sprite when the enemy moves in one direction, you may need to flip it back when it moves in the other direction.

Quiz: 2D Sprites

1) We have a 32x32 image with a 24x24 sprite. Which of the following will render the sprite as a 48x48 image? There are multiple correct answers.

A. ObjSprite2D_SetSourceRect(obj, 0, 0, 24, 24); ObjSprite2D_SetDestRect(obj, 0, 0, 48, 48);
B. ObjSprite2D_SetSourceRect(obj, 0, 0, 24, 24); ObjRender_SetScaleXYZ(obj, 2, 2, 1);
C. ObjSprite2D_SetSourceRect(obj, 0, 0, 24, 24); ObjSprite2D_SetDestCenter(obj);

2) Kanako is trying to render a graphic. However, it is completely off center! How can she rectify the situation? Her code is below. There are multiple correct answers.

ObjSprite2D_SetSourceRect(obj, 0, 0, 512, 512); ObjSprite2D_SetDestRect(obj, 0, 0, 512, 512);
A. Use ObjSprite2D_SetSourceRect(obj, -256, -256, 256, 256) instead of ObjSprite2D_SetSourceRect(obj, 0, 0, 512, 512)
B. Use ObjSprite2D_SetDestRect(obj, -256, -256, 256, 256) instead of ObjSprite2D_SetDestRect(obj, 0, 0, 512, 512)
C. Use ObjSprite2D_SetDestCenter(obj) instead of ObjSprite2D_SetDestRect(obj, 0, 0, 512, 512)

Part 6: What Are Render Priorities?

To close this lesson, we will have a brief discussion about render priorities, which are how Danmakufu (and programs in general) render images on top of each other.

In Danmakufu, there are render priorities from 0 to 100, and something rendered with a render priority of 40 will render above 39, for example. When rendering multiple images with the same render priority, whichever is created later will render above the one created earlier.

By default, render priorities of 20 and below, as well as 80 and above, use the top left of the window as their origin (0,0), while those in between use the top left of the playing field as their origin. Everything rendered in a standard plural or stage should be in the 20-80 range, while things rendered in packages should be rendered in the other ranges.

By default, the player is rendered at 30, 2D sprites for enemy objects are rendered at 40, 2D sprites for bullets are rendered at 50, 2D sprites for items are rendered at 60, and 69 is the limit for some camera focus priorities (don't worry about this last one until you work on backgrounds).

Pretty much all of these defaults can be changed using various functions, so don't worry if you want to change them, though they should be adequate for all practical purposes.

Anyways, there are two ways to set the render priority of a graphic: Obj_SetRenderPriority() and Obj_SetRenderPriorityI(). The first uses a 0.0 to 1.0 scale, and the latter uses the 0 to 100 scale I've been using. Decimal values do not work for the latter and will truncate to the integer portion - IE, you only have 101 render priority slots.

For now, keep render priorities in mind, as they will prove important later on. That concludes the lesson.

Summary

  • 2D Sprites have their own functions for determining their UV (ObjSprite2D_SetSourceRect()) and XY rects (ObjSprite2D_SetDestRect() and ObjSprite2D_SetDestCenter())
  • ObjSprite2D_SetDestRect() can be used to stretch and scale a 2D Sprite
  • A boss can be animated using a counter and either if/else/switch statements or by flooring the difference between the animation frame and the number of sprites
  • Objects with higher render priorities render above those with lower render priorities

Sources and External Resources

N/A