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实现游戏声音效果,让我们的游戏更加生动起来。