当前位置: 首页 > news >正文

基础项目实战——3D赛车(c++)

目录

  • 前言
  • 一、渲染引擎
  • 二、关闭事件
  • 三、梯形绘制
  • 四、轨道绘制
  • 五、边缘绘制
  • 六、草坪绘制
  • 七、前后移动
  • 八、左右移动​
  • 九、曲线轨道​
  • 十、课山坡轨道​
  • 十一、循环轨道​
  • 十二、背景展示​
  • 十三、引入速度​
  • 十四、物品绘制​
  • 十五、课数字路障​
  • 十六、分数展示​
  • 十七、重新生成​
  • 十八、跳跃功能​
  • 十九、课音效播放
  • 二十、音乐播放​
  • 二十一、音乐切换​

前言

这一期我们一起学期3d赛车这个项目实战,这个项目比之前的那几个难度高了很多,所以做起来会比较复杂,里面会涉及一些数学思想。这个项目做完了之后非常有意思还是值得一做的。

完整工程文件,提取码为1111
(蓝色字体可以点进去)

一、渲染引擎

做这个项目我们要自己到网上下载一些库,要配置环境。所以配置环境可以看我上一期扫雷(蓝色字体可以点进去)

#include <SFML/Graphics.hpp>
using namespace sf;const int WinWidth = 1024;const int WinHeight = 768;int main() {​
RenderWindow win(VideoMode(WinWidth, WinHeight), "Racing");​win.setFramerateLimit(60);  // 设置帧率为 60 帧​   
while (win.isOpen()) {}return 0;
}

运行之后我不得到一个不可以移动的窗口。

二、关闭事件

这样就可以关闭和移动窗口了。

    while (win.isOpen()) {​Event e;while (win.pollEvent(e)) {if (e.type == Event::Closed) {​win.close();}}}

三、梯形绘制

这个梯形就代表每一块道路的图案


void DrawTrape(RenderWindow& window, Color c, int x1, int y1, int w1, int x2, int y2, int w2) {ConvexShape polygon(4);//梯形的四个定点polygon.setFillColor(c);//设置梯形填充的颜色polygon.setPoint(0, Vector2f(x1 - w1, y1));polygon.setPoint(1, Vector2f(x2 - w2, y2));polygon.setPoint(2, Vector2f(x2 + w2, y2));polygon.setPoint(3, Vector2f(x1 + w1, y1));window.draw(polygon);
}
win.clear();DrawTrape(win, Color::White, WinWidth / 2, 500, 200, WinWidth / 2, 300, 100);​win.display();

在这里插入图片描述

四、轨道绘制

const int roadWidth = 1800;
const int roadSegLength = 180;
const int roadCount = 1884;

对于每个轨道,用一个结构体来实现,小写的 x,y,z 代表,每个轨道的中心,在 3D 世界中的坐标,大写的 X 和 Y,则是 3D 映射到 2D 时的屏幕坐标,大写的 W 代表这条轨道映射到屏幕后,它的宽度。​
实现一个带参构造函数,利用初始化列表进行初始化。​
然后实现一个 project 函数,代表 3D 到 2D 的映射,camX,camY,camZ 代表的是 3D 相机的位置,接下来我会讲一些数学的概念。听不懂没有关系,只要你能够自己跟着把代码写出来,并且通过修改每个参数,运行后得到你想要的效果就可以了。不用太计较这里的数学计算。​
首先,当相机位置 camZ 确定时,z 越大,那么这条轨道就应该越小,所以实现这么一个反比例函数,计算缩放比例 scale。​
利用这个缩放比例,可以计算出 X 和 Y,像这样。​
最后计算 W,也就是每个轨道的实际屏幕宽度,和 scale 强相关,所以作为系数乘上去。

struct Road {float x, y, z;float X, Y, W;float scale;Road(int _x, int _y, int _z): x(_x), y(_y), z(_z) {}void project(int camX, int camY, int camZ) {​scale = 1.0 / (z - camZ);​X = (1 + (x - camX) * scale) * WinWidth / 2;​Y = (1 - (y - camY) * scale) * WinHeight / 2;​W = scale * roadWidth * WinWidth / 2;}};

定义一个轨道的 vector,每个轨道的 x 和 y 都是 0,z 坐标按照 i 进行平铺。​

    vector<Road> roads;for (int i = 0; i < roadCount; ++i) {​
Road r(0, 0, (i+1) * roadSegLength);​roads.push_back(r);}

因为近大远小的关系,所以屏幕上只显示 300 个轨道,对于每个轨道,调用 project 计算投影,传入的是相机的位置,其中 y 代表竖直方向,设定为 1600。​
获取当前轨道和前一个轨道,分别为 now 和 pre,利用这两个轨道,可以计算出一个梯形,并且设置好颜色,稍微做点差异化,产生相间的效果。运行看下效果。​

 
for (int i = 0; i < 300; ++i) {​Road& now = roads[i % roadCount];​now.project(0, 1600, 0);if (!i) {continue;}​Road& pre = roads[(i - 1) % roadCount];​Color road = i % 2 ? Color(105, 105, 105) :DrawTrape(win, road, pre.X, pre.Y, pre.W, now.X, now.Y, now.W);}

当然,可以在 i 这里除上一个数,使得两个颜色区分的时候,不会那么密集。看下效果。这里如果你不理解,怎么办呢?你就多试几个数字,试着试着感觉就有了。​

Color road = (i/3) % 2 ? Color(105, 105, 105) : Color(101, 101, 101);

五、边缘绘制

Color edge = (i/3) % 2 ? Color(0, 0, 0) : Color(255, 255, 255);
DrawTrape(win, edge, pre.X, pre.Y, pre.W*1.3, now.X, now.Y, now.W*1.3);

六、草坪绘制

Color grass = i / 3 % 2 ? Color(16, 210, 16) : Color(0, 199, 0);
DrawTrape(win, grass, pre.X, pre.Y, WinWidth, now.X, now.Y, WinWidth);

七、前后移动

    for (int i = 0; i < roadCount; ++i) {​
Road r(0, 0, (i+1) * roadSegLength);​roads.push_back(r);}int cameraZ = 0;while (win.isOpen()) {if (Keyboard::isKeyPressed(Keyboard::Up)) cameraZ += roadSegLength;if (Keyboard::isKeyPressed(Keyboard::Down)) cameraZ -= roadSegLength;​win.clear();int roadIndex = cameraZ / roadSegLength;for (int i = roadIndex; i < roadIndex + 300; ++i) {​Road& now = roads[i % roadCount];​now.project(0, 1600, cameraZ );

八、左右移动​

除了前后移动,左右也可以移动。同样是控制相机的坐标,定义 cameraX 代表相机的 x坐标。​

int cameraZ = 0;int cameraX = 0;

分别按下左键和右键时,更改 cameraX 的值。​

if (Keyboard::isKeyPressed(Keyboard::Left)) cameraX -= 100;
if (Keyboard::isKeyPressed(Keyboard::Right)) cameraX += 100;


然后在这里把 cameraX 代替原来 0 的值。​

now.project(cameraX, 1600, cameraZ);

运行,发现露馅了。​没事,宽度乘10就好了。​

DrawTrape(win, grass, pre.X, pre.Y, 10*WinWidth, now.X, now.Y, 10*WinWidth);

九、曲线轨道​

这节课我们想办法让这条路,能够变成弯的,看起来更加的真实,在原来的路上,加上一个曲线因子。​

float scale, curve;Road(int _x, int _y, int _z, float _c): x(_x), y(_y), z(_z), curve(_c) {}



然后在实例化 Road 的时候,当 i 在 0 到 300 之间,曲线因子就是 0.5,否则就是负的 0.5。构造的时候,传参传进去。​

float curve =  (i > 0 && i < 300) ? 0.5 : -0.5;​Road r(0, 0, (i+1) * roadSegLength, curve);

然后定义一个 x 和 dx,初始化都为 0,这段代码怎么去理解呢?这里的 now.curve 就理解成加速度,那么 dx 就是速度,而 x 就是位移,这样这条路弯曲起来就看起来非常的连续了,看下效果。​

int roadIndex = cameraZ / roadSegLength;float x = 0, dx = 0;for (int i = roadIndex; i < roadIndex + 300; ++i) {​Road& now = roads[i % roadCount];​now.project(cameraX - x, 1600, cameraZ);​x += dx;​dx += now.curve;

十、课山坡轨道​

然后我希望这个轨道,有一种跌宕起伏的感觉,三角函数可以做到这一点,所以每个轨道的中心的 y 坐标,用一个三角函数 sin 来实现,振幅 1600,像这样:​

相机的位置从 1600 作为基准,再加上起始轨道的 y 坐标,像这样。​

int cameraY = 1600 + roads[roadIndex].y;int minY = WinHeight;


这里的 1600 就可以改成这个 cameraY 了。所以这个 cameraY 的范围,就根据正弦函数的值,变成了 0 到 3200。​

now.project(cameraX - x, cameraY, cameraZ);

这时候我们发现,效果好像不对,问题出在哪里呢?​
原因是这样的。​
需要注意的是,一旦出现跌宕起伏,那么如果出现一个 Y 值最小的轨道,后面比 Y 值大的轨道,就不应该被绘制出来,注意这里用的屏幕坐标系,所以越往上值越小。​

int roadIndex = cameraZ / roadSegLength;
float x = 0, dx = 0;int cameraY = 1600 + roads[roadIndex].y;int minY = WinHeight;

那么判断当前的 Y 是否比最小的 Y 小,如果小,那么记录这个最小值;否则直接 continue,代表这条轨道不用绘制,来看看效果。​

if (now.Y < minY) {​minY = now.Y;else {continue;}

十一、循环轨道​

然后我们希望整条路,能够产生循环,这样就可以,用有限的 Road 对象来展现无限的路。​
计算这条路的总长度 totalLength,如果发现 cameraZ 的位置已经大于总长度了,就减去总长度,如果小于总长度,就变成负数了,那么加上总长度。​
其实就是让 cameraZ 的值,始终保持在 [0, totalLength) 之间。​

int totalLength = roadCount * roadSegLength;while (cameraZ >= totalLength) cameraZ -= totalLength;while (cameraZ < 0) cameraZ += totalLength;
now.project(cameraX - x, cameraY, cameraZ - (i >= roadCount ? totalLength : 0) );

然后你会发现,这里出现了一些断层,这个不是很合理。​
根据 PI 计算一下,roadCount 让它是 3.14 的倍数就好了,那就 3.14 乘上 600,变成 1884 ,看下效果。​

const int roadCount = 1884;

十二、背景展示​

准备一张云的图片,加载一个纹理,并且生成一个精灵。宽度是窗口宽度,高度是窗口高度的一半。​

    Texture bg;​bg.loadFromFile("cloud.png");​
Sprite s(bg, IntRect(0, 0, WinWidth, WinHeight/2));

然后在每次绘制轨道的时候,把它绘制出来。​

if (now.Y < minY) {​minY = now.Y;}else {continue;}​
win.draw(s);

十三、引入速度​

引入一个赛车前进的速度,speed 初始化为 0。​

int cameraZ = 0;int cameraX = 0;int speed = 100;

当键盘按下 UP 和 DOWN 的时候,不再修改 cameraZ 的值,而是修改速度的值,然后最终,cameraZ 把这个速度累加上就好了。​
这样一来,往前往后,修改的就是速度,不再是位移。​

if (Keyboard::isKeyPressed(Keyboard::Up)) {​
if (speed > 1000) speed = 1000;​
}​
if (Keyboard::isKeyPressed(Keyboard::Down)) {​
speed -= 2;​
if (speed < 100) speed = 100;​
}​
cameraZ += speed;

十四、物品绘制​

接下来在这条路上,间隔的绘制物品,每个物品在图片的宽高是 450,这个作为常量,根据你图片的大小,进行修改即可。​

const int itemSize = 450;

Road 的结构体中,定义一个精灵 spr。​

struct Road {float x, y, z;float scale, curve;​Sprite spr;

实现一个 drawItem 的函数,设置好纹理的矩形,缩放比例,以及位置,位置的计算方式比较简单,X 坐标和这条路保持一致,Y 坐标减去 W。然后给它绘制出来。​

    s.setScale(W / itemSize, W / itemSize);​s.setPosition(X, Y - W);​win.draw(s);}

然后定义一个物品的纹理对象,加载对应的图片,生成一个精灵对象。​

    Texture item;​item.loadFromFile("item.png");​
Sprite sitem(item);

把精灵对象通过传参,传到 Road 对象中。​

Road r(0, sin(i/30.0)*1600, (i + 1) * roadSegLength, curve, sitem);


然后一个 for 循环,注意从后往前绘制,否则遮挡关系会不对。为了不产生密集恐惧症,我们可以 % 上一个 20,每 20 个轨道,出现一个物品。​

for (int i = roadIndex + 300; i > roadIndex; --i) {if (i % 20 == 0)​roads[i % roadCount].drawItem(win);}
 Road r(0, 0, (i + 1) * roadSegLength, curve, sitem);

十五、课数字路障​

我们把路上的这些物品,显示成一些不同的数字,有数字,有符号。​
首先定义这么一个字符串,和这张图片中的每个图片一一对应。​

const char charItem[] = "1234567890+*/-%";

然后在 Road 中定义两个变量,operatorIndex 代表符号在 charItem 中的下标,numberIndex 代表数字 charItem 中的下标。​

int operatorIndex;int numberIndex;


接下来就可以开始初始化了,为了数字不会太密集,采用随机数的方式,如果是 200 的倍数,才出现物品。同样是通过随机的方式,尽量不出现 0,因为 除 0 和 模 0 都没意义。​
operatorIndex 等于 -1 代表这个 Road 上没有东西。​

    Road(int _x, int _y, int _z, float _c, Sprite _spr): x(_x), y(_y), z(_z), curve(_c), spr(_spr){if (rand() % 200 == 0) {​operatorIndex = (rand() % 5) + 10;​numberIndex = rand() % 10;if (numberIndex == 9) {​numberIndex = 0;}}else {​operatorIndex = -1;}}

然后我们重载原先的 drawItem,并且加入一个新的参数 ,index 代表的是 charItem 中的下标,xPlacement 则表示 x 方向的偏移。然后 left 和 top 则代表的是在这张图上的(对应数字或者符号)的左上角坐标。​

void drawItem(RenderWindow& win, int index, int xPlacement) {​Sprite s = spr;int left = (index % 5) * itemSize;int top = (index / 5) * itemSize;​s.setTextureRect(IntRect(left, top, itemSize, itemSize));​s.setScale(W / itemSize, W / itemSize);​s.setPosition(X + xPlacement*W, Y - W);​win.draw(s);}void drawItem(RenderWindow& win) {if (operatorIndex == -1) {return;}drawItem(win, operatorIndex, -1);drawItem(win, numberIndex, 0);}


这里的取模就可以去掉了。​

for (int i = roadIndex + 300; i > roadIndex; --i) {​roads[i % roadCount].drawItem(win);}

十六、分数展示​

我们在窗口的左上角,展示一个分数,并且还是这张图片。先定义一个分数 score,尽量考虑到所有情况,所以初始化 - 的 1234567890。​

然后实现一个 DrawNumber 函数,sItem 代表数字对应那个精灵,x 和 y 代表显示在屏幕的左上角坐标。然后把 number 转换成字符串,存储到 ch 中。​
遍历这个字符串,并且去 charItem 中找到对应的下标,利用相同的绘制方式,把它绘制在屏幕上。​

void DrawNumber(RenderWindow& win, Sprite sItem, int number, int x, int y) {char ch[100] = {'\0'};_itoa_s(number, ch, 10);int len = strlen(ch);for (int i = 0; i < len; ++i) {​Sprite s = sItem;int index = -1;for (int j = 0; charItem[j]; ++j) {if (charItem[j] == ch[i]) {​index = j;break;}}int left = (index % 5) * itemSize;int top = (index / 5) * itemSize;​s.setTextureRect(IntRect(left, top, itemSize, itemSize));​s.setScale(0.18, 0.18);​s.setPosition(x + 0.13 * itemSize * i, y);​win.draw(s);}}


然后调用即可。​

    s.setTextureRect(IntRect(0, 0, WinWidth, minY));​win.draw(s);DrawNumber(win, sitem, score, 10, 10);

十七、重新生成​

如果每次路过都把数字清空,那么迟早有一天,路上就没有数字了,于是实现一个重新生成的接口,这里做一点小改动,这个 Road 一开始,是通过随机计算的。​
当然,如果 generateItem 直接传一个 true,那么必定产生数字。​

Road(int _x, int _y, int _z, float _c, Sprite _spr): x(_x), y(_y), z(_z), curve(_c), spr(_spr){generateItem(false);}void generateItem(bool bAlwaysGen) {if (bAlwaysGen || rand() % 200 == 0) {​operatorIndex = (rand() % 5) + 10;​numberIndex = rand() % 10;if (numberIndex == 9) {​numberIndex = 0;}}else {​operatorIndex = -1;}}

所以当前这条路如果正好出屏幕外,那么就把 i + 1500 的轨道,生成一个新的数字。​

now.operatorIndex = -1;​
roads[(i + 1500) % roadCount].generateItem(

十八、跳跃功能​

为了增加趣味性,可以引入跳跃功能,从而可以跳过这个数字。加入一个状态叫 isJumping ,一开始是 false,代表没有跳跃。​
跳跃会改变 z 的坐标,所以 z 代表实际偏移的 z ,dz 代表跳跃时的速度。​

bool isJumping = false;float z = 0, dz = 0;


如果按下空格,当非跳跃状态,则切换到跳跃状态,并且把起跳速度设置为 150。​

if (Keyboard::isKeyPressed(Keyboard::Space)) {if (isJumping == false) {​isJumping = true;​dz = 150;}}


然后如果一直在跳跃中,则减去 5,这里就是在模拟自由落体,这里的 5 就是加速度,然后 z 累加 dz 的过程,就是模拟的 位移 和 速度,当 z 小于等于 0,说明落地了,isJumping 置为 false。​

if (isJumping) {​dz -= 5;​z += dz;if (z <= 0) {​z = 0;​isJumping = false;}}


最后,当遇到数字的时候,如果非跳跃状态,才会生效,否则没有任何效果。​

if (now.Y >= WinHeight) {if (!isJumping && now.operatorIndex != -1) {​score = caculateScore(score, now.operatorIndex, now.numberIndex);​now.operatorIndex = -1;​roads[(i + 1500) % roadCount].generateItem(true);}}

十九、课音效播放

接下来,我们加入一些音效,让游戏更加的带感。引入一个头文件。​

初始化三个音效缓存,分别代表 碰到数字、起跳、落地 音效。​

buffer[1].loadFromFile("jump.mp3"); ​
buffer[2].loadFromFile("falldown.mp3");


起跳可以这么写。​

if (isJumping == false) {​sound.setBuffer(buffer[1]);​sound.play();​isJumping = true;​dz = 150;}

落地可以这么写。​

if (z <= 0) {​z = 0;​isJumping = false;​sound.setBuffer(buffer[2]);​sound.play();}

碰到数字可以这么写。​

if (!isJumping && now.operatorIndex != -1) {​score = caculateScore(score, now.operatorIndex, now.numberIndex);​now.operatorIndex = -1;​roads[(i + 1500) % roadCount].generateItem(true);​sound.setBuffer(buffer[0]);​sound.play();}

二十、音乐播放​

再加上一个 bgm。​

 SoundBuffer buffer[5];​Sound sound, bgm;​buffer[0].loadFromFile("get.mp3");​buffer[1].loadFromFile("jump.mp3"); ​buffer[2].loadFromFile("falldown.mp3");​buffer[3].loadFromFile("tianfuyue.mp3");​buffer[4].loadFromFile("liumaishenjian.mp3");


在窗口 while 之前,调用 play 接口。​

bgm.setBuffer(buffer[3]);​
bgm.setLoop(true);​
bgm.play();

二十一、音乐切换​

音乐可以随着赛车的速度,进行切换,当开的快的时候,播放 4 号 音乐;否则,播放 5 号音乐。​

if (Keyboard::isKeyPressed(Keyboard::Up)) {​speed += 2;if (speed > 1000) speed = 1000;if (speed == 500) {​speed = 502;​bgm.setBuffer(buffer[4]);​bgm.play();}}if (Keyboard::isKeyPressed(Keyboard::Down)) {​speed -= 2;if (speed < 100) speed = 100;if (speed == 500) {​speed = 498;​bgm.setBuffer(buffer[3]);​bgm.play();}}

相关文章:

基础项目实战——3D赛车(c++)

目录 前言一、渲染引擎二、关闭事件三、梯形绘制四、轨道绘制五、边缘绘制六、草坪绘制七、前后移动八、左右移动​九、曲线轨道​十、课山坡轨道​十一、循环轨道​十二、背景展示​十三、引入速度​十四、物品绘制​十五、课数字路障​十六、分数展示​十七、重新生成​十八、…...

ODP(OBProxy)路由初探

OBProxy路由策略 Primary Zone 路由 官方声明默认情况&#xff0c;会将租户请求发送到租户的 primary zone 所在的机器上&#xff0c;通过 Primary Zone 路由可以尽量发往主副本&#xff0c;方便快速寻找 Leader 副本。另外&#xff0c;设置primary zone 也会在一定成都上减少…...

从零推导线性回归:最小二乘法与梯度下降的数学原理

​ 欢迎来到我的主页&#xff1a;【Echo-Nie】 本篇文章收录于专栏【机器学习】 本文所有内容相关代码都可在以下仓库中找到&#xff1a; Github-MachineLearning 1 线性回归 1.1 什么是线性回归 线性回归是一种用来预测和分析数据之间关系的工具。它的核心思想是找到一条直…...

计算机网络__基础知识问答

Question: 1&#xff09;在计算机网络的5层结构中&#xff0c;每一层的功能大概是什么&#xff1f; 2&#xff09;交换机的功能&#xff1f;https://www.bilibili.com/video/BV1na4y1L7Ev 3&#xff09;路由器的功能&#xff1f;https://www.bilibili.com/video/BV1hv411k7n…...

第 5 章:声音与音乐系统

5.1 声音效果的应用 在游戏中&#xff0c;声音效果是增强游戏沉浸感和趣味性的重要元素。Pygame 提供了强大的音频处理功能&#xff0c;使得添加各种声音效果变得相对简单。声音效果可以包括角色的动作音效&#xff0c;如跳跃、攻击、受伤时的声音&#xff1b;环境音效&#x…...

C语言编译过程全面解析

今天是2025年1月26日&#xff0c;农历腊月二十七&#xff0c;一个距离新春佳节仅一步之遥的日子。城市的喧嚣中&#xff0c;年味已悄然弥漫——能在这个时候坚持上班的人&#xff0c;真可称为“牛人”了吧&#xff0c;哈哈。。。。 此刻&#xff0c;我在重新审视那些曾被遗忘的…...

算法每日双题精讲 —— 前缀和(【模板】一维前缀和,【模板】二维前缀和)

在算法竞赛与日常编程中&#xff0c;前缀和是一种极为实用的预处理技巧&#xff0c;能显著提升处理区间和问题的效率。今天&#xff0c;我们就来深入剖析一维前缀和与二维前缀和这两个经典模板。 一、【模板】一维前缀和 题目描述 给定一个长度为 n n n 的整数数组 a a a&…...

Maui学习笔记- SQLite简单使用案例02添加详情页

我们继续上一个案例&#xff0c;实现一个可以修改当前用户信息功能。 当用户点击某个信息时&#xff0c;跳转到信息详情页&#xff0c;然后可以点击编辑按钮导航到编辑页面。 创建项目 我们首先在ViewModels目录下创建UserDetailViewModel。 实现从详情信息页面导航到编辑页面…...

VMware 中Ubuntu无网络连接/无网络标识解决方法【已解决】

参考文档 Ubuntu无网络连接/无网络标识解决方法_ubuntu没网-CSDN博客 再我们正常使用VMware时&#xff0c;就以Ubuntu举例可能有时候出现无网络连接&#xff0c;甚至出现无网络标识的情况&#xff0c;那么废话不多说直接上教程 环境&#xff1a;无网络 解决方案&#…...

完美世界前端面试题及参考答案

如何设置事件捕获和事件冒泡? 在 JavaScript 中,可以通过addEventListener方法来设置事件捕获和事件冒泡。该方法接收三个参数,第一个参数是事件类型,如click、mousedown等;第二个参数是事件处理函数;第三个参数是一个布尔值,用于指定是否使用事件捕获机制。当这个布尔值…...

新时代架构SpringBoot+Vue的理解(含axios/ajax)

文章目录 引言SpringBootThymeleafVueSpringBootSpringBootVue&#xff08;前端&#xff09;axios/ajaxVue作用响应式动态绑定单页面应用SPA前端路由 前端路由URL和后端API URL的区别前端路由的数据从哪里来的 Vue和只用三件套axios区别 引言 我是一个喜欢知其然又知其所以然的…...

代理模式 -- 学习笔记

代理模式学习笔记 什么是代理&#xff1f; 代理是一种设计模式&#xff0c;用户可以通过代理操作&#xff0c;而真正去进行处理的是我们的目标对象&#xff0c;代理可以在方法增强&#xff08;如&#xff1a;记录日志&#xff0c;添加事务&#xff0c;监控等&#xff09; 拿一…...

gif动画图像优化,相同的图在第2,4,6帧中重复出现,会增加图像体积吗?

对于 GIF 图像&#xff0c;情况与 Git 文件存储有所不同。GIF 是一种图像格式&#xff0c;其体积主要取决于图像的内容、颜色数量、优化设置等因素。如果在 GIF 动画中&#xff0c;相同的图像在第 2、4、6 帧中重复出现&#xff0c;是否会增加图像体积&#xff0c;取决于以下几…...

Harmony Next 跨平台开发入门

ArkUI-X 官方介绍 官方文档&#xff1a;https://gitee.com/arkui-x/docs/tree/master/zh-cn ArkUI跨平台框架(ArkUI-X)进一步将ArkUI开发框架扩展到了多个OS平台&#xff1a;目前支持OpenHarmony、Android、 iOS&#xff0c;后续会逐步增加更多平台支持。开发者基于一套主代码…...

阿里巴巴Qwen团队发布AI模型,可操控PC和手机

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

android 音视频系列引导

音视频这块的知识点自己工作中有用到&#xff0c;一直没有好好做一个总结&#xff0c;原因有客观和主观的。 客观是工作太忙&#xff0c;没有成段时间做总结。 主观自己懒。 趁着这次主动离职拿了n1的钱&#xff0c;休息一下&#xff0c;对自己的人生做一下总结&#xff0c;…...

STM32调试手段:重定向printf串口

引言 C语言中经常使用printf来输出调试信息&#xff0c;打印到屏幕。由于在单片机中没有屏幕&#xff0c;但是我们可以重定向printf&#xff0c;把数据打印到串口&#xff0c;从而在电脑端接收调试信息。这是除了debug外&#xff0c;另外一个非常有效的调试手段。 一、什么是pr…...

基于 Jenkins 的测试报告获取与处理并写入 Jira Wiki 的技术总结

title: 基于 Jenkins 的测试报告获取与处理并写入 Jira Wiki 的技术总结 tags: - jenkins - python categories: - jenkins在软件开发的持续集成与持续交付&#xff08;CI/CD&#xff09;流程里&#xff0c;及时、准确地获取并分析测试报告对保障软件质量至关重要。本文将详细…...

Vue.js组件开发-实现导出PDF文件可自定义添加水印及水印样式方向

使用 Vue 实现导出 PDF 文件并添加水印&#xff0c;同时支持设置水印样式、方向和自定义水印内容。 步骤 安装依赖&#xff1a;使用 html2canvas 将 HTML 内容转换为 canvas&#xff0c;使用 jspdf 生成 PDF 文件。创建 Vue 组件&#xff1a;在组件中实现水印生成、HTML 转 c…...

css中的animation

css的animation animation是一个综合属性,是animation-name, animation-duration, animation-timing-function, animation-delay, animation-iteration-count, animation-direction, animation-fill-mode, animation-play-state, and animation-timeline这些属性的简写 不过在…...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数

高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

如何配置一个sql server使得其它用户可以通过excel odbc获取数据

要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据&#xff0c;你需要完成以下配置步骤&#xff1a; ✅ 一、在 SQL Server 端配置&#xff08;服务器设置&#xff09; 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到&#xff1a;SQL Server 网络配…...

[USACO23FEB] Bakery S

题目描述 Bessie 开了一家面包店! 在她的面包店里&#xff0c;Bessie 有一个烤箱&#xff0c;可以在 t C t_C tC​ 的时间内生产一块饼干或在 t M t_M tM​ 单位时间内生产一块松糕。 ( 1 ≤ t C , t M ≤ 10 9 ) (1 \le t_C,t_M \le 10^9) (1≤tC​,tM​≤109)。由于空间…...

TJCTF 2025

还以为是天津的。这个比较容易&#xff0c;虽然绕了点弯&#xff0c;可还是把CP AK了&#xff0c;不过我会的别人也会&#xff0c;还是没啥名次。记录一下吧。 Crypto bacon-bits with open(flag.txt) as f: flag f.read().strip() with open(text.txt) as t: text t.read…...

海云安高敏捷信创白盒SCAP入选《中国网络安全细分领域产品名录》

近日&#xff0c;嘶吼安全产业研究院发布《中国网络安全细分领域产品名录》&#xff0c;海云安高敏捷信创白盒&#xff08;SCAP&#xff09;成功入选软件供应链安全领域产品名录。 在数字化转型加速的今天&#xff0c;网络安全已成为企业生存与发展的核心基石&#xff0c;为了解…...

算法—栈系列

一&#xff1a;删除字符串中的所有相邻重复项 class Solution { public:string removeDuplicates(string s) {stack<char> st;for(int i 0; i < s.size(); i){char target s[i];if(!st.empty() && target st.top())st.pop();elsest.push(s[i]);}string ret…...

电脑桌面太单调,用Python写一个桌面小宠物应用。

下面是一个使用Python创建的简单桌面小宠物应用。这个小宠物会在桌面上游荡&#xff0c;可以响应鼠标点击&#xff0c;并且有简单的动画效果。 import tkinter as tk import random import time from PIL import Image, ImageTk import os import sysclass DesktopPet:def __i…...

React父子组件通信:Props怎么用?如何从父组件向子组件传递数据?

系列回顾&#xff1a; 在上一篇《React核心概念&#xff1a;State是什么&#xff1f;》中&#xff0c;我们学习了如何使用useState让一个组件拥有自己的内部数据&#xff08;State&#xff09;&#xff0c;并通过一个计数器案例&#xff0c;实现了组件的自我更新。这很棒&#…...

Linux操作系统共享Windows操作系统的文件

目录 一、共享文件 二、挂载 一、共享文件 点击虚拟机选项-设置 点击选项&#xff0c;设置文件夹共享为总是启用&#xff0c;点击添加&#xff0c;可添加需要共享的文件夹 查询是否共享成功 ls /mnt/hgfs 如果显示Download&#xff08;这是我共享的文件夹&#xff09;&…...