用 vue3 + phaser 实现经典小游戏:飞机大战

cb8a11e8bd3bbef243d0797ea0bbb72d.jpeg

a658413058f122948eb0676cdd217980.gif

本文字数:7539

预计阅读时间:30分钟

01

前言

说起小游戏,最经典的莫过于飞机大战了,相信很多同学都玩过。今天我们也来试试开发个有趣的小游戏吧!我们将从零开始,看看怎样一步步实现一个H5版的飞机大战!

首先我们定好目标,要做一个怎样的飞机大战,以及去哪整游戏素材?

刚好微信小程序官方提供了一个飞机大战小游戏的模板,打开【微信开发者工具】,选择【新建项目】-【小游戏】,选择飞机大战的模板,创建后就是一个小程序版飞机大战。

1704ad87855556aa4882da88d03193a5.png

运行小程序之后可以看到下面的效果:

115a965de65c1fe3611d37ef8d57dca3.gif

从运行效果上看,这个飞机大战已经比较完整,包含了以下内容:

1.地图滚动,播放背景音效;

2.玩家控制飞机移动;

3.飞机持续发射子弹,播放发射音效;

4.随机出现向下移动的敌军;

5.子弹碰撞敌军时,播放爆炸动画和爆炸音效,同时子弹和敌军都销毁,并增加1个得分;

6.飞机碰撞敌军时,游戏结束,弹出结束面板。

接下来我们以这个效果为参考,并拷贝这个项目中的图片和音效素材,从头做一个H5版飞机大战吧!

02

选择游戏框架

你可能会好奇,既然微信小程序官方已经生成好了完整代码,直接参考那套代码不就好吗?

这里就涉及到游戏框架的问题,小程序那套代码是没有使用游戏框架的,所以很多基础的地方都需要自己实现,比如说子弹移动,子弹与敌军碰撞检测等。

我们以碰撞为例,在小程序项目中是这样实现的:

1.先定义好碰撞检测的方法isCollideWith(),通过两个物体的坐标和宽高进行碰撞检测计算:

isCollideWith(sp) {let spX = sp.x + sp.width / 2;let spY = sp.y + sp.height / 2;if (!this.visible || !sp.visible) return false;return !!(spX >= this.x && spX <= this.x + this.width && spY >= this.y && spY <= this.y + this.height);
},

2.然后在每一帧的回调中,遍历所有子弹和所有敌军,依次调用isCollideWith()进行碰撞检测:

update() {bullets.forEach((bullet) => {for (let i = 0, il = enemys.length; i < il; i++) {if (enemys[i].isCollideWith(bullet)) {// Do Something}}});
}

3.而通过游戏框架,可能只需要一行代码。我们以Phaser为例:

this.physics.add.overlap(bullets, enemys, () => { // Do Something
}, null, this);

上面代码的含义是:bullets(子弹组)和enemys(敌军组)发生overlap(重叠)则触发回调。

从上面的例子可以看出,选择一个游戏框架来开发游戏,可以大大降低开发难度,减少代码量。

当开发一个专业的游戏时,我们一般会选择专门的游戏引擎,比如Cocos,Egret,LayaBox,Unity等。但是如果只是做一个简单的H5小游戏,嵌入我们的前端项目中,使用Phaser就可以了。

引用Phaser官网上的介绍:

【Phaser是一个快速、免费且有趣的开源HTML5游戏框架,可在桌面和移动Web浏览器上提供WebGL和Canvas渲染。可以使用第三方工具将游戏编译为iOS、Android和本机应用程序。您可以使用JavaScript或TypeScript进行开发。】

同时Phaser在社区也非常受欢迎,Github上收获35.5k的Star,Npm上最近一周下载量19k。

因此我们采用Phaser作为游戏框架。接下来,开始正式我们的飞机大战之旅啦!

03

准备工作

3.1 创建项目

项目采用的技术栈是:Phaser + Vue3 + TypeScript + Vite。

当然对于这个游戏来说,核心的框架是Phaser,其他都是可选的。只使用Phaser + Html也是可以开发的,只是我们希望采用目前更主流的开发方式。

进行工作目录,直接使用vue手脚架创建名为plane-war的项目。

npm create vue

项目创建完成,安装依赖,检查是否运行正常。

cd plane-war
npm install
npm run dev

接下来再安装phaser。

npm install phaser

3.2 整理素材

接下来我们重新整理下项目,清除不需要的文件,并把游戏素材拷贝到assets目录,最终目录结构如下:

plane-war
├── src
│   ├── assets
│   │   ├── audio
│   │   │   ├── bgm.mp3
│   │   │   ├── boom.mp3
│   │   │   └── bullet.mp3
│   │   ├── images
│   │   │   ├── background.jpg
│   │   │   ├── boom.png
│   │   │   ├── bullet.png
│   │   │   ├── enemy.png
│   │   │   ├── player.png
│   │   │   └── sprites.png
│   │   └── json
│   │       └── sprites.json
│   ├── App.vue
│   └── main.ts

素材处理1:

原本游戏素材中,爆炸动画是由19张独立图片组成,在Phaser中需要合成一张雪碧图,可以通过雪碧图合成工具合成,命名为boom.png,效果如下:

196d9eb820160c3513808605a3afae52.png

素材处理2:

原本游戏素材中,结束面板的图片来源一张叫Common.png的雪碧图,我们重命名为sprites.png。并且我们还需要为这个雪碧图制作一份说明,起名为sprites.json。通过它来指定我们需要用到目标图片及其在雪碧图中的位置。

这里我们指定2个目标图片,result是结束面板,button是按钮。

{"textures": [{"image": "sprites.png","size": {"w": 512,"h": 512},"frames": [{"filename": "result","frame": { "x": 0, "y": 0, "w": 119, "h": 108 }},{"filename": "button","frame": { "x": 120, "y": 6, "w": 39, "h": 24 }}]}]
}

3.3 初步运行

我们重构App.vue,创建了一个游戏对象game,指定父容器为#container,创建成功后则会在父容器中生成一个canvas 元素,游戏的所有内容都通过这个canvas进行呈现和交互。

<template><div id="container"></div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import { Game, AUTO, Scale } from "phaser";let game: Game;
onMounted(() => {game = new Game({parent: "container",type: AUTO,width: 375,// 高度依据屏幕宽高比计算height: (window.innerHeight / window.innerWidth) * 375,scale: {// 自动缩放至宽或高与父容器一致,类似css中的contain// 由于宽高比与屏幕宽高比一致,最终就是刚好全屏效果mode: Scale.FIT,},physics: {default: "arcade",arcade: {debug: false,},},});
});onUnmounted(() => {game.destroy(true);
});
</script>
<style>
body {margin: 0;
}
#app {height: 100%;
}
</style>

通过npm run dev再次运行项目,我们把浏览器展示区切换:为移动设备展示,此时可以看到canvas,并且其宽高应该正好全屏。

cb48c9979495fbddb6af06ad3224bcff.png

3.4 场景设计

可以看到现在画布还是全黑的,这是因为创建game对象时还没有接入任何场景。在Phaser中,一个游戏可以包含多个场景,而具体的游戏画面和交互都是在各个场景中实现的。

接下来我们设计3个场景:

  • 预载场景 :加载整个游戏资源,创建动画,展示等待开始画面。

  • 主场景:游戏的主要画面和交互。

  • 结束场景:展示游戏结束画面。

2ac74f5fad2664e4c2f773412f80d92d.jpeg

在项目中我们新增3个自定义场景类:

plane-war
├── src
│   ├── game
│   │   ├── Preloader.ts
│   │   ├── Main.ts
│   │   └── End.ts

自定义场景类继承Scene类,包含了以下基本结构:

import { Scene } from "phaser";export class Preloader extends Scene {constructor() {// 场景命名,这个命名在后面场景切换使用super("Preloader");}// 加载游戏资源preload() {}// preload中的资源全部加载完成后执行create() {}// 每一帧的回调update() {}
}

按上面的基本结构分别实现好3个场景类,并导入到game对象的创建中:

import { onMounted, onUnmounted } from "vue";
import { Game, AUTO, Scale } from "phaser";
import { Preloader } from "./game/Preloader";
import { Main } from "./game/Main";
import { End } from "./game/End";let game: Game;
onMounted(() => {game = new Game({// 其他参数省略...// 定义场景,默认初始化数组中首个场景,即 Preloaderscene: [Preloader, Main, End],});
});

04

预载场景

准备工作完成后,接下来我们开始真正开发第一个游戏场景:预载场景,对应Preloader.ts文件。

4.1 加载游戏资源

preload方法中加载整个游戏所需的资源。

import { Scene } from "phaser";
import backgroundImg from "../assets/images/background.jpg";
import enemyImg from "../assets/images/enemy.png";
import playerImg from "../assets/images/player.png";
import bulletImg from "../assets/images/bullet.png";
import boomImg from "../assets/images/boom.png";
import bgmAudio from "../assets/audio/bgm.mp3";
import boomAudio from "../assets/audio/boom.mp3";
import bulletAudio from "../assets/audio/bullet.mp3";export class Preloader extends Scene {constructor() {super("Preloader");}preload() {// 加载图片this.load.image("background", backgroundImg);this.load.image("enemy", enemyImg);this.load.image("player", playerImg);this.load.image("bullet", bulletImg);this.load.spritesheet("boom", boomImg, {frameWidth: 64,frameHeight: 48,});// 加载音频this.load.audio("bgm", bgmAudio);this.load.audio("boom", boomAudio);this.load.audio("bullet", bulletAudio);}create() {}
}

4.2 添加元素

接下来我们在create()方法中去添加背景,背景音乐,标题,开始按钮,后续使用的动画,并且为开始按钮绑定了点击事件。

const { width, height } = this.cameras.main;
// 背景
this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);
// 背景音乐
this.sound.play("bgm");// 标题
this.add.text(width / 2, height / 4, "飞机大战", {fontFamily: "Arial",fontSize: 60,color: "#e3f2ed",stroke: "#203c5b",strokeThickness: 6,}).setOrigin(0.5);// 开始按钮
let button = this.add.image(width / 2, (height / 4) * 3, "sprites", "button").setScale(3, 2).setInteractive().on("pointerdown", () => {// 点击事件:关闭当前场景,打开Main场景this.scene.start("Main");});// 按钮文案
this.add.text(button.x, button.y, "开始游戏", {fontFamily: "Arial",fontSize: 20,color: "#e3f2ed",}).setOrigin(0.5);// 创建动画,命名为 boom,后面使用
this.anims.create({key: "boom",frames: this.anims.generateFrameNumbers("boom", { start: 0, end: 18 }),repeat: 0,
});

运行效果如下:

733389cc10aae37b13d15e0ad5663449.png

有个细节可以留意下,就是这个背景是怎样铺满整个屏幕的?

上面的代码是this.add.tileSprite()创建了一个瓦片精灵,素材中的背景图就像一个一个瓦片一样铺满屏幕,所以就要求素材中的背景图是一张首尾能无缝相连的图片,这样就能无限平铺。主场景中的背景移动也是基于此。

05

主场景

5.1 梳理场景元素

在预载场景中点击“开始游戏”按钮,可以看到画面又变成黑色,此时预载场景被关闭,游戏打开主场景。

在主场景中,涉及到的场景元素一共有:背景、玩家、子弹、敌军、爆炸,我们可以先尝试把它们都渲染出来,并加一些简单的动作,比如移动背景,子弹和敌军添加垂直方向速度,播放爆炸动画等。

import { Scene, GameObjects, type Types } from "phaser";// 场景元素
let background: GameObjects.TileSprite;
let enemy: Types.Physics.Arcade.SpriteWithDynamicBody;
let player: Types.Physics.Arcade.SpriteWithDynamicBody;
let bullet: Types.Physics.Arcade.SpriteWithDynamicBody;
let boom: GameObjects.Sprite;export class Main extends Scene {constructor() {super("Main");}create() {const { width, height } = this.cameras.main;// 背景background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);// 玩家this.physics.add.sprite(100, 600, "player").setScale(0.5);// 子弹this.physics.add.sprite(100, 500, "bullet").setScale(0.25).setVelocityY(-100);// 敌军this.physics.add.sprite(100, 100, "enemy").setScale(0.5).setVelocityY(100);// 爆炸this.add.sprite(200, 100, "boom").play("boom");}update() {// 设置背景瓦片不断移动background.tilePositionY -= 1;}
}

效果如下:

20fa8241e27a5e4d2c86c6581761d12d.gif

看起来似乎已经有了雏形,但是这里还需要优化一下代码设计。我们不希望场景中的所有元素创建,交互都糅合Main.ts这个文件中,这样就显得有点臃肿,不好维护。

我们再设计出:玩家类、子弹类、敌军类、炸弹类,让每个元素它们自身的事件和行为都各自去实现,而主场景只负责创建它们,并且处理它们之间的交互事件,不需要去关心它们内部的实现。

虽然这个游戏的整体代码也不多,但是通过这个设计思想,可以让我们的代码设计更加合理,当以后开发其他更复杂的小游戏时也可以套用这种模式。

6aff7204438698398b77449617cd0c71.jpeg

5.2 玩家类

回顾上面的创建玩家的代码:

this.physics.add.sprite(100, 600, "player").setScale(0.5);

原本的代码是直接创建了一个“物理精灵对象“,我们现在改成新建一个Player类,这个类继承Physics.Arcade.Sprite,然后在主场景中通过new Player()也同样生成"物理精灵对象"。相当于Player类拓展了原本Physics.Arcade.Sprite,增加了对自身的一些事件处理和行为封装。后续的子弹类,敌军类等也是同样的方式。

Player类主要拓展了"长按移动事件",具体实现如下:

import { Physics, Scene } from "phaser";export class Player extends Physics.Arcade.Sprite {isDown: boolean = false;downX: number;downY: number;constructor(scene: Scene) {// 创建对象let { width, height } = scene.cameras.main;super(scene, width / 2, height - 80, "player");scene.add.existing(this);scene.physics.add.existing(this);// 设置属性this.setInteractive();this.setScale(0.5);this.setCollideWorldBounds(true);// 注册事件this.addEvent();}addEvent() {// 手指按下我方飞机this.on("pointerdown", () => {this.isDown = true;// 记录按下时的飞机坐标this.downX = this.x;this.downY = this.y;});// 手指抬起this.scene.input.on("pointerup", () => {this.isDown = false;});// 手指移动this.scene.input.on("pointermove", (pointer) => {if (this.isDown) {this.x = this.downX + pointer.x - pointer.downX;this.y = this.downY + pointer.y - pointer.downY;}});}
}

5.3 子弹类

Bullet类主要拓展了"发射子弹"和"子弹出界事件",具体实现如下:

import { Physics, Scene } from "phaser";export class Bullet extends Physics.Arcade.Sprite {constructor(scene: Scene, x: number, y: number, texture: string) {// 创建对象super(scene, x, y, texture);scene.add.existing(this);scene.physics.add.existing(this);// 设置属性this.setScale(0.25);}// 发射子弹fire(x: number, y: number) {this.enableBody(true, x, y, true, true);this.setVelocityY(-300);this.scene.sound.play("bullet");}// 每一帧更新回调preUpdate(time: number, delta: number) {super.preUpdate(time, delta);// 子弹出界事件(子弹走到顶部超出屏幕)if (this.y <= -14) {this.disableBody(true, true);}}
}

5.4 敌军类

Enemy类主要拓展了"生成敌军"和"敌军出界事件",具体实现如下:

import { Physics, Math, Scene } from "phaser";export class Enemy extends Physics.Arcade.Sprite {constructor(scene: Scene, x: number, y: number, texture: string) {// 创建对象super(scene, x, y, texture);scene.add.existing(this);scene.physics.add.existing(this);// 设置属性this.setScale(0.5);}// 生成敌军born() {let x = Math.Between(30, 345);let y = Math.Between(-20, -40);this.enableBody(true, x, y, true, true);this.setVelocityY(Math.Between(150, 300));}// 每一帧更新回调preUpdate(time: number, delta: number) {super.preUpdate(time, delta);let { height } = this.scene.cameras.main;// 敌军出界事件(敌军走到底部超出屏幕)if (this.y >= height + 20) {this.disableBody(true, true)}}
}

5.5 爆炸类

Boom 类主要拓展了"显示爆炸"和“隐藏爆炸”,具体实现如下:

import { GameObjects, Scene } from "phaser";export class Boom extends GameObjects.Sprite {constructor(scene: Scene, x: number, y: number, texture: string) {super(scene, x, y, texture);// 爆炸动画播放结束事件this.on("animationcomplete-boom", this.hide, this);}// 显示爆炸show(x: number, y: number) {this.x = x;this.y = y;this.setActive(true);this.setVisible(true);this.play("boom");this.scene.sound.play("boom");}// 隐藏爆炸hide() {this.setActive(false);this.setVisible(false);}
}

5.6 重构主场景

上面我们实现了玩家类,子弹类,敌军类,爆炸类,接下来我们在主场景中重新创建这些元素,并加入分数文本元素。

import { Scene, Physics, GameObjects } from "phaser";
import { Player } from "./Player";
import { Bullet } from "./Bullet";
import { Enemy } from "./Enemy";
import { Boom } from "./Boom";// 场景元素
let background: GameObjects.TileSprite;
let player: Player;
let enemys: Physics.Arcade.Group;
let bullets: Physics.Arcade.Group;
let booms: GameObjects.Group;
let scoreText: GameObjects.Text;// 场景数据
let score: number;export class Main extends Scene {constructor() {super("Main");}create() {let { width, height } = this.cameras.main;// 创建背景background = this.add.tileSprite(0, 0, width, height, "background").setOrigin(0, 0);// 创建玩家player = new Player(this);// 创建敌军enemys = this.physics.add.group({frameQuantity: 30,key: "enemy",enable: false,active: false,visible: false,classType: Enemy,});// 创建子弹bullets = this.physics.add.group({frameQuantity: 15,key: "bullet",enable: false,active: false,visible: false,classType: Bullet,});// 创建爆炸booms = this.add.group({frameQuantity: 30,key: "boom",active: false,visible: false,classType: Boom,});// 分数score = 0;scoreText = this.add.text(10, 10, "0", {fontFamily: "Arial",fontSize: 20,});// 注册事件this.addEvent();},update() {// 背景移动background.tilePositionY -= 1;}
}

需要注意的是,这里的子弹,敌军,爆炸都是按组创建的,这样我们可以直接监听子弹组和敌军组的碰撞,而不需要监听每一个子弹和每一个敌军的碰撞。另一方面,创建组时已经把组内的元素全部创建好了,比如创建敌军时指定frameQuantity: 30,表示直接创建30个敌军元素,后续敌军不断出现和销毁其实就是这30个元素在循环使用而已,而并非源源不断地创建新元素,以此减少性能损耗。

最后再把注册事件实现,主场景就全部完成了。

// 注册事件
addEvent() {// 定时器this.time.addEvent({delay: 400,callback: () => {// 生成2个敌军for (let i = 0; i < 2; i++) {enemys.getFirstDead()?.born();}// 发射1颗子弹bullets.getFirstDead()?.fire(player.x, player.y - 32);},callbackScope: this,repeat: -1,});// 子弹和敌军碰撞this.physics.add.overlap(bullets, enemys, this.hit, null, this);// 玩家和敌军碰撞this.physics.add.overlap(player, enemys, this.gameOver, null, this);
}
// 子弹击中敌军
hit(bullet, enemy) {// 子弹和敌军隐藏enemy.disableBody(true, true);bullet.disableBody(true, true);// 显示爆炸booms.getFirstDead()?.show(enemy.x, enemy.y);// 分数增加scoreText.text = String(++score);
}
// 游戏结束
gameOver() {// 暂停当前场景,并没有销毁this.sys.pause();// 保存分数this.registry.set("score", score);// 打开结束场景this.game.scene.start("End");
}

06

结束场景

最后再实现一下结束场景,很简单,主要包含结束面板,得分,重新开始按钮。

import { Scene } from "phaser";export class End extends Scene {constructor() {super("End");}create() {let { width, height } = this.cameras.main;// 结束面板this.add.image(width / 2, height / 2, "sprites", "result").setScale(2.5);// 标题this.add.text(width / 2, height / 2 - 85, "游戏结束", {fontFamily: "Arial",fontSize: 24,}).setOrigin(0.5);// 当前得分let score = this.registry.get("score");this.add.text(width / 2, height / 2 - 10, `当前得分:${score}`, {fontFamily: "Arial",fontSize: 20,}).setOrigin(0.5);// 重新开始按钮let button = this.add.image(width / 2, height / 2 + 50, "sprites", "button").setScale(3, 2).setInteractive().on("pointerdown", () => {// 点击事件:关闭当前场景,打开Main场景this.scene.start("Main");});// 按钮文案this.add.text(button.x, button.y, "重新开始", {fontFamily: "Arial",fontSize: 20,}).setOrigin(0.5);}
}

07

优化

经过上面的代码,整个游戏已经基本完成。不过在测试的时候,感觉玩家和敌军还存在一定距离就触发了碰撞事件。在创建game时,我们可以打开debug模式,这样就可以看到Phaser为我们提供的一些调试信息。

game = new Game({physics: {default: "arcade",arcade: {debug: true,},},// ...
});

测试一下碰撞:

659a49a34156e429b8f85b929e561111.png

可以看到两个元素的边框确实发生碰撞了,但是这并不符合我们的要求,我们希望两个飞机看起来是真的挨到一起才触发碰撞事件。所以我们可以再优化一下,飞机本身不变,但是边框缩小。

Player.ts的构造函数中追加如下:

export class Player extends Physics.Arcade.Sprite {constructor() {// ...// 追加下面一行this.body.setSize(120, 120);}
}

Enemy.ts的构造函数中追加如下:

export class Enemy extends Physics.Arcade.Sprite {constructor() {// ...// 追加下面一行this.body.setSize(100, 60);}
}

最终可以看到边框已经被缩小,效果如下:

bd0449c9774f541645e1c55f0881fae9.png

08

结语

至此,飞机大战全部开发完成。

回顾一下开发过程,我们先搭建项目,创建游戏对象,接下来又设计了:预载场景、主场景、结束场景,并且为了减少主场景的复杂度,我们以场景元素的维度,将涉及到的场景元素进行封装,形成:玩家类、子弹类、敌军类、爆炸类,让这些场景元素各自实现自身的事件和行为。

在Phaser中的场景元素又可以分为普通元素和物理元素,物理元素是来自Physics,其中玩家类,子弹类,敌军类都是物理元素,物理元素具有物理属性,比如重力,速度,加速度,弹性,碰撞等。

在本文代码中涉及到了很多Phaser的API,介于篇幅没有一一解释,但是很多通过字面意思也可以理解,比如说disableBody表示禁用元素,setVelocityY表示设置Y 轴方向速度。并且我们也可以通过编译器的代码提示功能去了解这些方法的说明和参数含义:

66091e0ad6ef2fe1bd9084fe31ea805f.png

最后,本文的所有代码都已上传gitee,有兴趣的同学可以拉取代码看下。

演示效果:https://yuhuo.online/plane-war/(点击"阅读原文"访问链接)

源码地址:https://gitee.com/yuhuo520/plane-war

2e26c0512a6576830f535367bf07f682.jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/15113.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【pyspark速成专家】4_Spark之RDD编程2

目录 四&#xff0c;常用PairRDD的转换操作 五&#xff0c;缓存操作 四&#xff0c;常用PairRDD的转换操作 PairRDD指的是数据为长度为2的tuple类似(k,v)结构的数据类型的RDD,其每个数据的第一个元素被当做key&#xff0c;第二个元素被当做value. reduceByKey #reduceByKey…

如何参与github开源项目并提交PR

&#x1f47d;System.out.println(“&#x1f44b;&#x1f3fc;嗨&#xff0c;大家好&#xff0c;我是代码不会敲的小符&#xff0c;目前工作于上海某电商服务公司…”); &#x1f4da;System.out.println(“&#x1f388;如果文章中有错误的地方&#xff0c;恳请大家指正&…

高速公路定向广播(声光一体) HT-600D

1、产品概述&#xff1a; HT-600D声光一体平面波IP定向广播是北京恒星科通创新性研发产品&#xff0c;采用公司自主研发的平面波传声技术&#xff0c;该产品具有高声压、强指向性、高清晰度等特点&#xff0c;采用定向声传声技术将声音聚集到正前方定向传输,周边声压级明显降低…

YOLOV10实时端到端目标检测

代码地址&#xff1a;GitHub - THU-MIG/yolov10: YOLOv10: Real-Time End-to-End Object Detection 论文地址&#xff1a;https://arxiv.org/pdf/2405.14458 本文介绍了YOLO系列目标检测器在实时和高效方面的优势&#xff0c;但是仍然存在一些缺陷&#xff0c;包括依赖非极大值…

React useState修改对象

在 React 中&#xff0c;useState 是一个 Hook&#xff0c;它可以让函数组件拥有状态。当想要改变一个对象类型的状态时&#xff0c;我们需要使用展开运算符&#xff08;...&#xff09;或者 Object.assign 来确保状态是正确地更新。 以下是一个使用 useState 来更新对象的例子…

webstorm新建vue项目相关问题

前言 这个迭代后端需求偏少&#xff0c;前端code的键盘都起火星子了。来了4个外包支持&#xff0c;1个后端3个前端&#xff0c;还是不够用啊。刚好趁这个机会稍微学习下vue&#xff0c;其实之前环境也配置过了&#xff0c;所以这里就不分享环境配置了&#xff0c;主要分享下新建…

Java开发大厂面试第22讲:Redis 是如何保证系统高可用的?它的实现方式有哪些?

高可用是通过设计&#xff0c;减少系统不能提供服务的时间&#xff0c;是分布式系统的基础也是保障系统可靠性的重要手段。而 Redis 作为一款普及率最高的内存型中间件&#xff0c;它的高可用技术也非常的成熟。 我们今天分享的面试题是&#xff0c;Redis 是如何保证系统高可用…

什么是组态?什么是工业控制中的组态软件?

随着工业4.0和智能制造的发展&#xff0c;工控软件的应用越来越广泛&#xff0c;它们在提高生产效率、降低能耗和减少人力成本等方面发挥着越来越重要的作用。 什么是工控软件&#xff1f; 工控软件是指用于工业控制系统的软件&#xff0c;主要应用于各种生产过程控制、自动化…

PLSQL连接Linux Oracle21c

PLSQL连接Linux Oracle21c 一、安装PLsql 下载官网 https://www.allroundautomations.com/registered-plsqldev/ 二、Oracle Instant Client下载 使用plsql连接oracle的时候是需要本地先安装oracle客户端&#xff0c;英文名就是Oracle Instant Client。 官方下载地址&…

初出茅庐的小李博客之用MQTT.fx软件进行消息发布与订阅【 基于EMQX Cloud】

MQTT.fx软件使用简单介绍 MQTT.fx 的软件界面如下图所示&#xff0c;最上方为 MQTT Broker 连接地址栏&#xff0c;及其连接配置。其下方功能 Tabs 含有 Publish 发布栏、Subscribe 订阅栏、Scripts 脚本栏、Broker Status 状态消息栏、Log 日志信息控制栏。 连接之前要明确几…

【Linux系列】软链接使用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

深入编程逻辑:从分支到循环的奥秘

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、编程逻辑的基石&#xff1a;分支与循环 分支逻辑详解 代码案例&#xff1a;判断整数是…

UE5 双手握剑的实现(逆向运动学IK)

UE5 双手握剑的实现 IK 前言 什么是IK&#xff1f; UE官方给我们提供了很多对于IK处理的节点&#xff0c;比如ABRIK、Two Bone IK、Full Body IK 、CCD IK等&#xff0c;但是看到这&#xff0c;很多人就好奇了&#xff0c;什么是IK&#xff1f; 首先我们来看看虚幻小白人的骨…

[图解]产品经理创新之阿布思考法

0 00:00:00,000 --> 00:00:01,900 那刚才我们讲到了 1 00:00:02,730 --> 00:00:03,746 业务序列图 2 00:00:03,746 --> 00:00:04,560 然后怎么 3 00:00:05,530 --> 00:00:06,963 画现状&#xff0c;怎么改进 4 00:00:06,963 --> 00:00:09,012 然后改进的模式…

一条命令安装Metasploit Framework

做安全渗透的人都或多或少的使用kali-Linux系统中msfconsole命令启动工具&#xff0c;然而也经常会有人遇到这样那样的问题无法启动 今天我们就用一条命令来重新安装这个工具 curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/met…

Redis机制-Redis互斥锁、分布式锁

目录 一 互斥锁 二 分布式锁 Redis实现分布式锁 redisson实现分布式锁 可重入性&#xff1a; 主从一致性&#xff08;性能差&#xff09;&#xff1a; 一 互斥锁 假设我们现在有一个业务要实现秒杀优惠券的功能&#xff0c;如果是一个正常的流程&#xff0c;线程之间应该…

阅读笔记——《未知协议状态机推断技术研究综述》

【参考文献】盛嘉杰, 牛胜杰, 陈阳, 等. 未知协议状态机推断技术研究综述[J]. 计算机与现代化, 2023 (05): 58.【注】本文仅为作者个人学习笔记&#xff0c;如有冒犯&#xff0c;请联系作者删除。 摘要 协议逆向工程&#xff08;PRE&#xff09;描述了协议的行为逻辑&#xff…

spring cloud config server源码学习(一)

文章目录 1. 注解EnableConfigServer2. ConfigServerAutoConfiguration2.1 ConditionalOnBean和ConditionalOnProperty2.2 Import注解2.2.1. EnvironmentRepositoryConfiguration.class2.2.2. CompositeConfiguration.class2.2.3. ResourceRepositoryConfiguration.class2.2.4.…

shell命令运行原理及Linux权限问题

目录 shell命令以及运行原理用户管理添加用户删除用户sudo Linux权限的概念Linux权限管理文件访问者的分类&#xff08;人&#xff09;文件类型和访问权限&#xff08;事物属性&#xff09;文件权限值的表示方法文件访问权限的相关设置方法 目录的权限粘滞位 shell命令以及运行…

备考AMC8和AMC10竞赛,吃透2000-2024年1850道真题和解析(持续)

多做真题&#xff0c;吃透真题和背后的知识点是备考AMC8、AMC10有效的方法之一&#xff0c;通过做真题&#xff0c;可以帮助孩子找到真实竞赛的感觉&#xff0c;而且更加贴近比赛的内容&#xff0c;可以通过真题查漏补缺&#xff0c;更有针对性的补齐知识的短板。 今天我们继续…