JAVA一分钟游戏编程-游戏库改造之SDL3(09)

1.目标

一分钟实现游戏库改造之SDL3

2.C++封装SDL3

上一节,我们已经用SDL3的JNA调用显示了一个窗口,现在让我们根据Pinea的API方法定义,逐一绑定本地库吧。

2.1 头文件

我们现在Pinea.h头文件声明方法如下:

// 防止头文件被重复包含
#pragma once

// 引入SDL3核心库和图像库头文件
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>

// 定义导出/导入宏,用于DLL编译
// 当定义PINEA_BUILD_DLL时(编译DLL时),使用__declspec(dllexport)导出符号
// 否则(使用DLL时),使用__declspec(dllimport)导入符号
#ifdef PINEA_BUILD_DLL
#define PINEA_API __declspec(dllexport)
#else
#define PINEA_API __declspec(dllimport)
#endif

// 事件结构体,用于传递输入事件信息
struct PINEA_API PineaEvent {
    int type;  // 事件类型(如键盘按下、窗口关闭等)
    int key;   // 按键代码(当事件类型为键盘事件时有效)
};

// 颜色结构体,包含RGBA四个通道
struct PINEA_API PineaColor {
    int r;  // 红色通道(0-255)
    int g;  // 绿色通道(0-255)
    int b;  // 蓝色通道(0-255)
    int a;  // alpha通道(透明度,0-255)
};

// 矩形结构体,复用SDL的SDL_FRect(浮点精度矩形)
// 包含x, y(左上角坐标)和w, h(宽高)
typedef SDL_FRect PineaRect;

// 图像结构体,封装SDL纹理指针和图像尺寸信息
struct SDLPineaImage{
    SDL_Texture* imagePointer;  // SDL纹理指针(修正拼写错误:iamgePointer -> imagePointer)
    int width;                  // 图像宽度
    int height;                 // 图像高度
};

// 使用extern "C"包裹,确保C++编译时按C语言规则生成符号(避免名称修饰)
extern "C" {
    // 初始化Pinea引擎,创建窗口和渲染器
    // 参数:窗口标题、窗口宽度、窗口高度
    PINEA_API void pineaInit(const char* title, int width, int height);

    // 退出Pinea引擎,释放所有资源
    PINEA_API void pineaQuit();

    // 显示窗口(通常在初始化后调用)
    PINEA_API void pineaShow();

    // 处理事件队列,获取下一个事件
    // 参数:事件指针(用于存储获取到的事件)
    // 返回值:是否成功获取事件(true表示有事件,false表示无事件)
    PINEA_API bool pineaPollEvent(PineaEvent* event);

    // 清空渲染目标,使用指定颜色填充
    // 参数:颜色指针(指定清空颜色)
    PINEA_API void pineaClear(const PineaColor* color);

    // 执行渲染,将渲染缓冲区内容显示到屏幕
    PINEA_API void pineaRender();

    // 绘制填充矩形
    // 参数:矩形指针(位置和大小)、颜色指针(填充颜色)
    PINEA_API void pineaFillRect(const PineaRect* rect, const PineaColor* color);

    // 加载图像文件到SDLPineaImage结构体
    // 参数:文件路径、图像结构体指针(用于存储加载结果)
    PINEA_API void pineaLoadImage(const char* path, SDLPineaImage* sdlPineaImage);

    // 绘制图像
    // 参数:SDL纹理指针、源矩形(图像中要绘制的区域,nullptr表示整图)
    //       目标矩形(屏幕上的绘制位置和大小)、旋转角度、翻转模式
    PINEA_API void pineaDrawImage(SDL_Texture* image, const PineaRect* srcRect, const PineaRect* dstRect, double angle, int flip);

    // 释放图像资源(销毁SDL纹理)
    // 参数:要释放的SDL纹理指针
    PINEA_API void pineaDropImage(SDL_Texture* image);
}

2.2 pineaInit

// 全局窗口和渲染器指针,用于在整个程序中访问 SDL 窗口和渲染器
// SDL_Window*:SDL 窗口对象指针,管理窗口的创建、显示、尺寸等属性
// SDL_Renderer*:SDL 渲染器指针,用于执行绘制操作
SDL_Window* window;
SDL_Renderer* renderer;

void pineaInit(const char* title, int width, int height) {
    // 初始化 SDL 的视频子系统
	SDL_Init(SDL_INIT_VIDEO);
	window = SDL_CreateWindow(title, width, height, SDL_WINDOW_HIDDEN);
	renderer = SDL_CreateRenderer(window, nullptr);
    // 设置渲染器的混合模式为 alpha 混合
    // SDL_BLENDMODE_BLEND:启用透明度混合,使绘制的图形 / 图像支持半透明效果
	SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
}

2.3 pineaQuit

void pineaQuit() {
	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);
}

2.4 pineaShow

void pineaShow() {
	SDL_ShowWindow(window);
}

2.5 pineaPollEvent

bool pineaPollEvent(PineaEvent* event) {
	SDL_Event e;
    // 获取事件,获取成功保存到event
	if (SDL_PollEvent(&e)) {
		event->type = e.type;
		event->key = e.key.key;
		return true;
	}
	return false;
}

2.6 pineaClear

void pineaClear(const PineaColor* color) {
    // 设置颜色清屏
	SDL_SetRenderDrawColor(renderer, color->r, color->g, color->b, color->a);
	SDL_RenderClear(renderer);
}

2.7 pineaRender

void pineaRender() {
    // 刷新绘制
	SDL_RenderPresent(renderer);
}

2.8 pineaFillRect

void pineaFillRect(const PineaRect* rect, const PineaColor* color) {
    // 设置颜色填充矩形
	SDL_SetRenderDrawColor(renderer, color->r, color->g, color->b, color->a);
	SDL_RenderFillRect(renderer, rect);
}

2.9 pineaLoadImage

void pineaLoadImage(const char* path, SDLPineaImage* sdlPineaImage) {
    // 加载图片
	SDL_Texture* image = (SDL_Texture*)IMG_LoadTexture(renderer, path);
    // 设置图片Scale的模式为Nearest
	SDL_SetTextureScaleMode(image, SDL_SCALEMODE_NEAREST);
	sdlPineaImage->imagePointer = image;
	sdlPineaImage->width = image->w;
	sdlPineaImage->height = image->h;
}

2.10 pineaDrawImage

void pineaDrawImage(SDL_Texture* image, 
    const PineaRect* srcRect, 
    const PineaRect* dstRect, 
    double angle, int flip) {
	// 这里和Swing有点不一样,绘制图片时,可以一起设置图片的区域srcRect,
    // 并设置位置大小dstRect,角度angle,水平垂直翻转flip
    SDL_RenderTextureRotated(renderer, image, 
        srcRect, dstRect, angle, nullptr, (SDL_FlipMode)flip);
}

2.11 pineaDropImage

void pineaDropImage(SDL_Texture* image) {
    // 与Swing不一样,这里需要多一个销毁图像的方法
	SDL_DestroyTexture(image);
}

2.12 编译成动态库

保存代码,编译成功:

3.使用Pinea本地库

3.1 JNA方法声明

C++库已经编译成功,接下来,我们使用JNA来绑定本地库:

package com.sdl3.core;

import com.sdl3.render.PineaColor;
import com.sdl3.event.PineaEvent;
import com.sdl3.shape.PineaRect;
import com.sdl3.render.SDLPineaImage;
import com.sun.jna.Library;
import com.sun.jna.Pointer;

public interface PineaLib extends Library {

     void pineaInit(String title, int width, int height);
     void pineaQuit();
     void pineaShow();
     boolean pineaPollEvent(PineaEvent event);
     void pineaClear(PineaColor color) ;
     void pineaRender();
     void pineaFillRect(PineaRect rect, PineaColor color);
     void pineaLoadImage(String path, SDLPineaImage sdlPineaImage);
     void pineaDrawImage(Pointer imagePointer, PineaRect srcRect, 
            PineaRect dstRect, double angle, int flip);
     void pineaDropImage(Pointer imagePointer);

}

PineaLib类中的定义和Pinea.h中的方法一对一,但是我们之前说了,我不想管底层如何实现,换句话说,底层如何实现不应该对外暴露,像Pointer这种JNA定义的类我们就不要用了。

因此,我们需要在这个基础上继续包装一层,创建Pinea类:

package com.sdl3.core;

import com.sdl3.event.PineaEvent;
import com.sdl3.render.PineaColor;
import com.sdl3.render.PineaImage;
import com.sdl3.render.SDLPineaImage;
import com.sdl3.shape.PineaRect;
import com.sun.jna.Native;
import com.sun.jna.Pointer;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class Pinea {

    public static final int PINEA_EVENT_KEY_UP = 0x301;
    public static final int PINEA_EVENT_KEY_DOWN = 0x300;
    public static final int PINEA_EVENT_QUIT = 0x100;

    public static final int PINEA_KEY_A = 0x00000061;
    public static final int PINEA_KEY_D = 0x00000064;
    public static final int PINEA_KEY_W = 0x00000077;
    public static final int PINEA_KEY_S = 0x00000073;
    public static final int PINEA_KEY_SPACE = 0x00000020;

    private static PineaLib lib;
	// 用于缓存加载过的图像
    private static Map<String, SDLPineaImage> imageMap;

    static {
        // 设置jna库路径
        System.setProperty("jna.library.path", "csrc/bin");
        // 加载Pinea动态库
        lib = Native.load("Pinea", PineaLib.class);
	
        imageMap = new ConcurrentHashMap<>();
    }

    public static void pineaInit(String title, int width, int height) {
        lib.pineaInit(title, width, height);
    }

    public static void pineaQuit() {
        imageMap.forEach((k, v) -> {
            lib.pineaDropImage(v.imagePointer);
        });
        lib.pineaQuit();
    }

    public static void pineaShow() {
        lib.pineaShow();
    }

    public static boolean pineaPollEvent(PineaEvent event) {
        return lib.pineaPollEvent(event);
    }

    public static void pineaClear(PineaColor color) {
        lib.pineaClear(color);
    }

    public static void pineaRender() {
        lib.pineaRender();
    }

    public static void pineaFillRect(PineaRect rect, PineaColor color) {
        lib.pineaFillRect(rect, color);
    }

    public static PineaImage pineaLoadImage(String path) {
        // 如果imageMap有图像,则直接返回,否则加入到imageMap
        SDLPineaImage sdlPineaImage = imageMap.computeIfAbsent(path, p -> {
            SDLPineaImage temp = new SDLPineaImage();
            lib.pineaLoadImage(path, temp);
            if (temp.imagePointer == Pointer.NULL) {
                throw new RuntimeException("加载图片: " + path + "失败");
            }
            return temp;
        });
        PineaRect region = new PineaRect(0, 0, sdlPineaImage.width, sdlPineaImage.height);
        return new PineaImage(sdlPineaImage.imagePointer, region);
    }

    public static void pineaDrawImage(PineaImage image, float x, float y) {
        PineaRect dstRect = new PineaRect(x, y,
                image.getWidth() * image.getScaleX(), 
                image.getHeight() * image.getScaleY());
        lib.pineaDrawImage(image.getData(), 
                image.getRegion(), dstRect, image.getAngle(), image.getFlip());
    }

}

3.2 其他基本类

由于其他类都比较简单,我基本没写注释,稍微复杂一点的就是PineaImage定义,不过也还好,它自身的crop、flip、scale等方法定义和Swing实现的PineaImage完全一样。

PineaEvent:

import com.sun.jna.Structure;

import java.util.Arrays;
import java.util.List;

public class PineaEvent extends Structure implements Structure.ByReference {
    
    public int type;
    public int key;

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList("type", "key");
    }
}

PineaRect:

import com.sun.jna.Structure;

import java.util.Arrays;
import java.util.List;

public class PineaRect extends Structure implements Structure.ByReference {
    public float x, y, w, h;

    public PineaRect(float x, float y, float w, float h) {
        this.x = x;
        this.y = y;
        this.w = w;
        this.h = h;
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList("x", "y", "w", "h");
    }
}

PineaColor:

import com.sun.jna.Structure;

import java.util.Arrays;
import java.util.List;

public class PineaColor extends Structure implements Structure.ByReference {
    
    public int r, g, b, a;

    public PineaColor(int r, int g, int b, int a) {
        this.r = r;
        this.g = g;
        this.b = b;
        this.a = a;
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList("r", "g", "b", "a");
    }
}

PineaImage

package com.sdl3.render;

import com.sdl3.shape.PineaRect;
import com.sun.jna.Pointer;

public class PineaImage {

    private Pointer imagePointer;
    private PineaRect region;
    private float scaleX;
    private float scaleY;
    private double angle;
    private int flip;

    private static final int PINEA_FLIP_HORIZONTAL = 1;
    private static final int PINEA_FLIP_VERTICAL = 2;

    public PineaImage(Pointer imagePointer, PineaRect region) {
        this.imagePointer = imagePointer;
        this.region = region;
        this.scaleX = 1.0f;
        this.scaleY = 1.0f;
        this.angle = 0.0;
        this.flip = 0;
    }

    public Pointer getData() {
        return imagePointer;
    }

    public int getWidth() {
        return (int) region.w;
    }

    public int getHeight() {
        return (int) region.h;
    }

    public PineaRect getRegion() {
        return region;
    }

    public float getScaleX() {
        return scaleX;
    }

    public float getScaleY() {
        return scaleY;
    }

    public double getAngle() {
        return angle;
    }

    public int getFlip() {
        return flip;
    }

    public PineaImage scale(float x, float y) {
        PineaImage image = copyImage(this);
        image.scaleX = x;
        image.scaleY = y;
        return image;
    }

    public PineaImage rotate(double angle) {
        PineaImage image = copyImage(this);
        image.angle = angle;
        return image;
    }


    public PineaImage flip(boolean flipX, boolean flipY) {
        PineaImage image = copyImage(this);
        if (!flipX && !flipY) {
            return image;
        }
        int flip = 0;
        // SDL3翻转可以用按位|,这样可以同时进行水平、垂直翻转
        if (flipX) {
            flip |= PINEA_FLIP_HORIZONTAL;
        }
        if (flipY) {
            flip |= PINEA_FLIP_VERTICAL;
        }
        image.flip = flip;
        return image;
    }

    public PineaImage crop(int x, int y, int width, int height) {
        PineaImage image = copyImage(this);
        image.region.x = x;
        image.region.y = y;
        image.region.w = width;
        image.region.h = height;
        return image;
    }

    private PineaImage copyImage(PineaImage source) {
        PineaRect region = new PineaRect(source.region.x, source.region.y, source.region.w, source.region.h);
        PineaImage image = new PineaImage(source.imagePointer, region);
        image.scaleX = source.scaleX;
        image.scaleY = source.scaleY;
        image.angle = source.angle;
        image.flip = source.flip;
        return image;
    }
}

SDLPineaImage:

import com.sun.jna.Pointer;
import com.sun.jna.Structure;

import java.util.Arrays;
import java.util.List;

public class SDLPineaImage extends Structure implements Structure.ByReference {
    
    // 对应SDL3中的SDL_Texture*
    public Pointer imagePointer;
    // 图片的原始尺寸
    public int width;
    public int height;

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList("imagePointer", "width", "height");
    }
}

4.游戏动画测试

通过这几节篇章,我们已经用SDL3实现了Pinea库,除了底层实现逻辑不一样,API调用和Swing实现的Pinea库保持高度一致。

当然,如果你觉得JNA调用很麻烦,你完全可以写C++程序。

我们可以将之前的游戏动画代码原封不动搬过来测试,并修改import的包名:

package com.sdl3;

import com.sdl3.event.PineaEvent;
import com.sdl3.render.PineaImage;
import com.sdl3.shape.PineaRect;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.sdl3.core.Pinea.*;

public class Main {

    long startTime; // 记录程序启动时间,用于动画帧计算

    // 构造方法:初始化游戏窗口
    public Main() {
        pineaInit("PineaSDL3Test", 800, 600); // 初始化窗口,标题和尺寸
        startTime = System.currentTimeMillis(); // 记录启动时间
        init(); // 初始化游戏资源
    }

    // 游戏主循环
    public void run() {
        PineaEvent event = new PineaEvent(); // 事件对象,用于处理输入

        // 时间控制变量,用于稳定帧率
        long lastTime = System.nanoTime();
        long accTime = 0;
        long currentTime;
        long deltaTime = 1000_000_000 / 60; // 60FPS 的每帧纳秒数
        float deltaTimeF = 1f / 60f; // 每帧的时间(秒)

        pineaShow(); // 显示窗口
        boolean running = true;
        while (running) { // 主循环
            // 处理事件队列
            while (pineaPollEvent(event)) {
                if (event.type == PINEA_EVENT_QUIT) { // 窗口关闭事件
                    running = false;
                } else if (event.type == PINEA_EVENT_KEY_DOWN) { // 按键按下
                    Input.update(event.key, true);
                } else if (event.type == PINEA_EVENT_KEY_UP) { // 按键释放
                    Input.update(event.key, false);
                }
            }

            // 帧率控制:固定 60FPS 更新
            currentTime = System.nanoTime();
            accTime += currentTime - lastTime;
            lastTime = currentTime;
            if (accTime >= deltaTime) {
                while (accTime >= deltaTime) {
                    accTime -= deltaTime;
                    update(deltaTimeF); // 更新游戏逻辑
                }
                render(); // 渲染画面
                pineaRender(); // 提交渲染
            }
        }

        pineaQuit(); // 退出游戏
    }

    // 游戏资源
    PineaImage bgImage; // 背景图片
    Map<String, List<PineaImage>> playerAnimations; // 玩家动画集合(状态 -> 帧列表)
    PineaRect playerPosition; // 玩家位置和大小
    String playerState; // 玩家当前状态(idle/run/jump)
    boolean flipX; // 玩家是否水平翻转

    // 初始化游戏资源
    private void init() {
        bgImage = pineaLoadImage("assets/images/bg.png"); // 加载背景图

        // 加载玩家图片并切割动画帧
        PineaImage playerImage = pineaLoadImage("assets/images/mario.png");
        playerAnimations = new HashMap<>();
        // idle动画帧
        playerAnimations.put("idle", Arrays.asList(
                playerImage.crop(16 * 6, 16 * 2, 16, 16).scale(2.68f, 2.68f)
        ));
        // idle( flip)动画帧
        playerAnimations.put("flip_idle", Arrays.asList(
                playerImage.crop(16 * 6, 16 * 2, 16, 16).scale(2.68f, 2.68f).flip(true, false)
        ));
        // 跑步动画帧
        playerAnimations.put("run", Arrays.asList(
                playerImage.crop(16 * 0, 16 * 2, 16, 16).scale(2.68f, 2.68f),
                playerImage.crop(16 * 1, 16 * 2, 16, 16).scale(2.68f, 2.68f),
                playerImage.crop(16 * 2, 16 * 2, 16, 16).scale(2.68f, 2.68f)
        ));
        // 跑步( flip)动画帧
        playerAnimations.put("flip_run", Arrays.asList(
                playerImage.crop(16 * 0, 16 * 2, 16, 16).scale(2.68f, 2.68f).flip(true, false),
                playerImage.crop(16 * 1, 16 * 2, 16, 16).scale(2.68f, 2.68f).flip(true, false),
                playerImage.crop(16 * 2, 16 * 2, 16, 16).scale(2.68f, 2.68f).flip(true, false)
        ));
        // 跳跃动画帧
        playerAnimations.put("jump", Arrays.asList(
                playerImage.crop(16 * 4, 16 * 2, 16, 16).scale(2.68f, 2.68f)
        ));
        // 跳跃( flip)动画帧
        playerAnimations.put("flip_jump", Arrays.asList(
                playerImage.crop(16 * 4, 16 * 2, 16, 16).scale(2.68f, 2.68f).flip(true, false)
        ));

        playerState = "idle"; // 初始状态为 idle
        playerPosition = new PineaRect(0, 0, 0, 0); // 初始位置
    }

    // 更新游戏逻辑
    private void update(float deltaTimeF) {
        playerState = "idle"; // 默认状态为 idle

        // 根据按键更新玩家状态和位置
        if (Input.isKeyPressed(PINEA_KEY_A)) { // 左移
            playerPosition.x -= 250.f * deltaTimeF;
            playerState = "run";
            flipX = true;
        }
        if (Input.isKeyPressed(PINEA_KEY_D)) { // 右移
            playerPosition.x += 250.f * deltaTimeF;
            playerState = "run";
            flipX = false;
        }
        if (Input.isKeyPressed(PINEA_KEY_W)) { // 上移
            playerPosition.y -= 250.f * deltaTimeF;
            playerState = "run";
        }
        if (Input.isKeyPressed(PINEA_KEY_S)) { // 下移
            playerPosition.y += 250.f * deltaTimeF;
            playerState = "run";
        }
        if (Input.isKeyPressed(PINEA_KEY_SPACE)) { // 跳跃
            playerState = "jump";
        }
        // 如果翻转,后续就播放翻转动画
        if (flipX) {
            playerState = "flip_" + playerState;
        }
    }

    // 渲染画面
    private void render() {
        pineaDrawImage(bgImage, 0, 0); // 绘制背景

        // 根据当前状态获取动画帧列表
        List<PineaImage> pineaImages = playerAnimations.get(playerState);
        // 计算当前应该显示的帧索引(基于运行时间)
        long spendTime = System.currentTimeMillis() - startTime;
        int index = (int) spendTime / 100 % pineaImages.size();
        // 绘制玩家当前帧
        pineaDrawImage(pineaImages.get(index), playerPosition.x, playerPosition.y);
    }

    // 程序入口
    public static void main(String[] args) {
        new Main().run();
    }
}

运行:

没有任何问题。后续你可能想不断地优化库的底层实现,但是对外暴露的方法维持不变,让用户使用起来无感知,所以,如何让API方法的作用和参数设计合理是个值得考究的问题。

参数简单?职责单一?多个方法串联时(比如先LoadImage,再DrawImage)也不要太复杂?

王富贵:“谁知道呢,还得继续摸索。我认为目前的Pinea库有待完善,聪明如你一定能做得更好吧,期待你的设计😀。”

张平安:“哥们加油,我们拭目以待😎!”

5.中章

邪宗红雾诡地。

李吉祥手持SDL3黑色锻刀,周身黑气缭绕,目光如钉般死死地锁在王富贵、张平安两人身上。

“李师弟,原来你没事啊,这把刀是……”

张平安收起护盾,正欲上前打招呼,只觉前方刮起一股劲风,下一瞬,李吉祥形如鬼魅地出现在他身侧,同时挥动手中锻刀。

锻刀划过,有凌厉杀伐之意,攻势果断,竟要直取张平安脑袋。

张平安没有防备,眼看避之不及。

“小心。”

王富贵一把将张平安拉向后方,接着刺出手中JNI玄铁剑。

“铿!”

剑与刀的短暂碰撞,激荡出强烈的灵气波动。

旋即相互分开,双方退回安全距离。

张平安心有余悸道:“李师弟,你干什么?”

“眼前之人已经不是李师弟了!”王富贵脸色凝重。

“什么?”

张平安有些难以置信。

“你感受他的气息,身上已经没有JVM净化过的安全血脉了,而是充斥着邪宗的各种指针、析构函数、宏定义。”

李吉祥忽然大笑,“哈哈哈,王师兄不愧是王师兄,果然见多识广。”

语落,李吉祥的脸变得狰狞起来,只见他大喝一声,周身两团黑气快速转动,隐约间能看到黑气中闪烁着血色字符。

王富贵张平安二人定睛一看。

“取地址进行指针运算。”

“delete释放堆内存。”

张平安震惊:“我靠,这两个好像被宗门的那些老怪物们列为禁术了。我记得JVM练气篇中有提到过,这个伤敌八百,自损一千啊!”

而在这时,两团黑气突然钻入李吉祥的身体内。

李吉祥身体一震,随后仰天痛苦嘶吼,他的眼白逐渐沦为一片乌黑,脸部变得扭曲,浑身披头散发,状如癫狂。

穷人靠变异?

“张师弟,JNA符宝已在打开通道时消耗,你先退到一旁,待我为你争取时间重新凝聚,我们一定要救出李师弟,不能让他误入歧途。”

张平安重重点头,开始闭眼掐诀。

“王师兄竟能使用JNI玄铁剑这种绝世神兵,就是不知道威力如何,且让师弟讨教一番。”

李吉祥狞笑着,直逼王富贵而去。

“音爆——裂空鲸鸣!”

随着李吉祥一刀挥出,肉眼可见的实质般音波激射而出,化作一道半透明的气浪轰向王富贵。

“这是……SDL3Mixer的音波攻击?”

王富贵不敢托大,眼观鼻,鼻观心,蓦地举剑过顶。

“破障剑法——Native调用之Mix_Pause!”

……

6.思考:如何播放游戏音乐?

既然是游戏,怎么会少得了游戏音乐呢,下一期,我们将使用SDL3Mixer实现游戏声音效果,让我们的游戏更加生动起来。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值