cocos creator 帧同步游戏示例

news2024/11/17 0:06:15

最近闲来无事重新研究一下同步策略里面的帧同步,首先说下帧同步与状态同步的区别,

1:帧同步:

帧同步是一种多人游戏中常用的网络同步技术,用于确保不同玩家之间的游戏状态保持一致。在帧同步中,所有玩家通过按照相同的帧序列执行游戏逻辑和渲染,以达到一致的游戏状态。

以下是游戏帧同步的基本流程:

  1. 服务器发送帧数据:游戏中的服务器作为主机负责生成游戏的帧数据,并将其发送给所有客户端。帧数据包含了当前游戏状态的所有必要信息,例如玩家位置、物体状态等。

  2. 客户端接收帧数据:客户端接收服务器发送的帧数据,并根据接收到的数据进行游戏逻辑的模拟和渲染。

  3. 客户端帧同步:客户端根据接收到的帧数据,按照服务器发送的帧序列顺序执行游戏逻辑和渲染。这确保了所有客户端按照相同的顺序和状态进行游戏,保持一致性。

  4. 玩家输入同步:每个客户端将玩家的输入信息发送给服务器,服务器收集并处理所有玩家的输入。

  5. 服务器逻辑处理:服务器根据接收到的玩家输入信息,执行游戏逻辑的处理。这确保了所有客户端的游戏状态基于相同的输入。

  6. 更新帧数据:服务器在执行完游戏逻辑后,生成新的帧数据,并将其发送给所有客户端,重复上述过程。

通过帧同步,所有玩家在不同的客户端上可以看到相同的游戏状态,保证了游戏的公平性和一致性。

 

优点:

  1. 公平性:帧同步确保所有玩家在不同客户端上看到的游戏状态是一致的,避免了不公平的情况出现。

  2. 可预测性:由于所有客户端按照相同的帧序列执行游戏逻辑,因此玩家可以预测其他玩家的行动和结果,增加了策略性和竞争性。

  3. 简化网络通信:帧同步只需要传输帧数据和玩家输入,而不需要传输游戏中的每个操作和状态变化,减少了网络通信的数据量和开销。对于回放比较友好

  4. 允许离线和断线重连:帧同步允许玩家在游戏进行中离线或断线后重新加入,因为玩家的状态和输入都是根据帧数据进行同步的。

缺点:

  1. 网络延迟和不稳定性:网络延迟会导致帧同步的延迟和卡顿,尤其在玩家之间的网络连接质量不稳定时。较高的延迟会影响游戏的响应性和实时性。

  2. 带宽和数据量要求:帧同步需要在服务器和客户端之间频繁传输帧数据和玩家输入,因此对网络带宽和数据量要求较高。对于大规模多人游戏或网络条件较差的情况,可能会增加网络负担和成本。

  3. 复杂性和同步问题:帧同步的实现涉及到复杂的同步机制和算法,包括处理网络延迟、处理输入预测、补偿和插值等。这些机制需要仔细设计和调整,以确保游戏的一致性和流畅

2:状态同步

下面是状态同步的一般流程:

  1. 玩家输入收集:每个客户端收集本地玩家的输入信息,例如按键操作、鼠标移动等。这些输入信息通常以事件的形式记录,并在适当的时机发送给服务器。

  2. 服务器处理输入:服务器接收到玩家的输入信息后,执行相应的游戏逻辑和计算。服务器可以模拟玩家的操作并更新游戏状态。

  3. 游戏状态更新:服务器根据执行游戏逻辑的结果,更新游戏中的物体状态、位置、属性等信息。

  4. 状态广播:服务器将更新后的游戏状态广播给所有客户端。广播通常采用网络通信协议,如TCP或UDP,以确保状态更新的可靠传输。

  5. 客户端接收和应用状态:各个客户端接收到服务器广播的游戏状态更新,将其应用于本地的游戏模拟和渲染。客户端根据接收到的状态更新,更新游戏场景、角色位置、动画等。

通过以上流程,玩家的输入在服务器端进行处理和计算,并将结果广播给所有客户端,从而实现多人游戏状态的同步。

在状态同步的过程中,需要考虑网络延迟、带宽限制和数据压缩等因素,以确保状态更新的实时性和效率。一些优化技术,如差值插值、预测和补偿等,可以用于减少延迟和平滑状态的变化,提供更好的游戏体验。

3:下面的示例是使用了帧同步的策略来做的

本示例服务端使用了colyseus 第三方网络同步方案,客户端使用cocos creator2.4.8

注意要确保服务端的colyseus的版本和客户端的colyseus版本适配,否则容易造成版本的不兼容导致的报错,具体报错可以看看我之前发的文章:colyseus的常见报错原因。

怎么引入colyseus我就不具体讲了,自行在官网搜索就可以了。

a: 客户端处理colyseus服务端的消息类:

import Player from "../../server/src/rooms/entity/Player";
import { MyRoomState } from "../../server/src/rooms/schema/MyRoomState";
import BallPlayer from "./BallPlayer";
import BallGameData, { FrameData, FrameDataItem, FrameListData, ballGameData } from "./GameData";
import { gameManager } from "./GameManager";
import BaseComp from "./common/ui/baseComp";
import { deepClone } from "./common/utils/util";

const { ccclass, property } = cc._decorator;

/**
 * 
 * 
 * 帧同步和状态同步结合的方式进行同步
 * 
 * 帧同步:连接建立完毕后 frameIndex = 0 每隔一段时间,服务器广播当前帧的状态,客户端收到后,更新当前帧的状态
 * 
 */
@ccclass("NetworkManager")
export default class NetworkManager extends BaseComp {

    @property hostname = "localhost";
    @property port = 2567;
    @property useSSL = false;

    public client: Colyseus.Client = null;
    public room: Colyseus.Room<MyRoomState> = null;

    /** 客户端缓存的所有帧数据 */
    public frameList: FrameListData[] = [];

    private serverFrameRate: number = 20;

    /** 默认16ms的帧速率 */
    public frameSpeed = 16;

    /** 服务端帧插值 */
    private serverFrameAcc: number = 3;

    /** 当前游戏进行到了第几帧 */
    private _frameIndex: number = 0;

    private lastTickTimeOut: any = null;

    public get frameIndex() {
        return this._frameIndex;
    }

    private set frameIndex(f: number) {
        this._frameIndex = f;
    }

    /** 房间是否初始化完毕 */
    public roomIsInit: boolean = false;

    private interval: any = null;

    __preload(): void {
        this.openFilter = true;
        super.__preload();
        gameManager.networkManager = this;
    }

    onLoad() {
        const url = `${this.useSSL ? "wss" : "ws"}://${this.hostname}${([443, 80].includes(this.port) || this.useSSL) ? "" : `:${this.port}`}`;
        console.log("url si ", url);
        // @ts-ignore
        this.client = new Colyseus.Client(url);
        console.log("client is ", this.client);
        // @ts-ignore
        console.log("colyseus version is ", Colyseus.VERSION);
        this.connect();
    }

    async connect() {
        try {
            console.log("joinOrCreate is ", this.client.joinOrCreate);
            this.room = await this.client.joinOrCreate<MyRoomState>("my_room");
            console.log("room is ", this.room);
            console.log(this.room.state.playerMap);
            /** 停止游戏 */
            cc.game.pause();

            this.room.send("link", { frame: this.frameIndex });
            this.roomIsInit = true;
            // 向服务端询问游戏所有帧数据
            this.room.send("all_frames");
            this.interval = setInterval(this.sendCmd.bind(this), 1000 / 60);

            this.room.state.playerMap.onAdd = (player, sessionId) => {
                console.log("player added ", player, " sessionId is ", sessionId);
                return function () {
                    return true;
                }
            };

            this.room.onStateChange((state) => {
                console.log('state change is ', state);
                this.updatePlayerInfo(state.playerMap);
            });

            // this.room.state.playerMap.onChange = (player, sessionId) => {

            // }

            this.room.onLeave((code) => {
                console.log('leave is ', code);
            });

            this.room.onMessage("*", this._messageHandler.bind(this));

        } catch (e) {
            console.log("e is ", e);
        }
    }

    /** 客户端以特定时间发送指令集 */
    sendCmd() {
        if (!this.roomIsInit) return;
        const player = gameManager.playerManager.playerMap.get(ballGameData.selfSesssioId);
        if (!player) return;
        // 获取当前帧的指令集
        const cmd = gameManager.joystickManager.getCmd();
        if (cmd) {
            this.room.send("cmd", cmd);
        }
    }

    /** 更新玩家信息 */
    updatePlayerInfo(players: any) {
        players.forEach((v, k) => {
            if (v) {
                if (v.id == ballGameData.selfSesssioId) {
                    // 更新自己的位置
                    return;
                }
                let player = gameManager.playerManager.playerMap.get(v.id);
                if (!player) {
                    gameManager.playerManager.pushPlayer(v.id);
                    player = gameManager.playerManager.playerMap.get(v.id);
                }
                const playerComp = player.getComponent(BallPlayer);
                playerComp.updatePlayerInfo(v);
            }
        })
    }

    /**
     * 服务端消息
     * @param  {any} type
     * @param  {any} message
     */
    _messageHandler(type: any, message: any) {
        switch (type) {
            case "joinSuccess":
                ballGameData.selfSesssioId = message;
                if (gameManager.playerManager.playerMap.get(ballGameData.selfSesssioId)) return;
                gameManager.playerManager.pushPlayer(ballGameData.selfSesssioId);
                break;
            case "f":
                // 当前帧的数据 服务器每隔50ms发送一次 客户端需要进行补帧操作
                this.receiveFrameData(message);
                break;
            case "all_frames":
                /** 游戏服务器上的所有帧数据 */
                this.receiveAllFrameData(message);
                // 执行自己的游戏循环
                this.nextTick();
                break;
            case "bye":
                break;
        }
    }

    /**
     * 接收服务器的帧数据
     */
    receiveFrameData(data: Array<any>) {
        for (let i = 0, len = data.length; i < len; i++) {
            const frameIndex = data[0];
            this.frameList[frameIndex] = data[1] as FrameListData;
            if (!data[1]) {
                this.frameList[frameIndex] = [];
            }
            // 客户端对帧数据进行补帧操作 预测
            this.prediction(frameIndex, this.frameList[frameIndex]);
        }
    }

    /**
     * 接收服务端所有的帧数据
     * @param  {Array<Array<any>>} data 帧数据数组
     */
    receiveAllFrameData(data: Array<Array<any>>) {
        for (let i = 0, len = data.length; i < len; i++) {
            const frameIndex = data[i][0];
            this.frameList[frameIndex] = data[i][1] as FrameListData;
            if (!data[i][1]) {
                this.frameList[frameIndex] = [];
            }
            // 客户端对帧数据进行补帧操作 预测
            this.prediction(frameIndex, this.frameList[frameIndex]);
        }
        console.log("服务端所有帧数据:", this.frameList);
    }

    /**
     * 预测 补帧
     * @param  {number} frameIndex 当前帧的索引
     * @param  {FrameListData} serverFrameData 当前帧的数据
     */
    prediction(frameIndex: number, frameData: FrameListData) {
        for (let i = 1; i <= this.serverFrameAcc - 1; i++) {
            if (!this.frameList[frameIndex + i]) {
                this.frameList[frameIndex + i] = frameData;
            }
        }
    }

    /**
     * 执行当前帧
     */
    runTick() {
        let frame = null;
        if (this.frameList.length > 1) {
            frame = this.frameList[this.frameIndex];
        }

        // 如果frame 为null的话 说明当前帧没有数据 服务端也没有,说明客户端的帧快于服务端
        if (frame) {
            // console.log(`帧索引:${this.frameIndex}, 帧数据:${frame}`);
            if (frame.length > 0) {
                frame.forEach((item: FrameData) => {
                    const player = gameManager.playerManager.playerMap.get(item.id);
                    const actionData = item.data;
                    if (player) {
                        const playerComp = player.getComponent(BallPlayer);
                        playerComp.updatePlayerFromServer(actionData);
                    }
                });
            }
            // 前进帧
            this.frameIndex++;
            cc.game.step();
        } else {
            // console.warn("没有帧数据:", frame);
        }

    }

    nextTick() {
        this.lastTickTimeOut && clearTimeout(this.lastTickTimeOut);
        this.runTick();
        if (this.frameList.length - this.frameIndex > 100) {
            console.log("追帧...");
            this.frameSpeed = 0;
        } else if (this.frameList.length - this.frameIndex > 3) {
            this.frameSpeed = 0;
        } else {
            this.frameSpeed = 1000 / (this.serverFrameRate * (this.serverFrameAcc + 1));
        }
        // 16ms调用一次nextTick()
        this.lastTickTimeOut = setTimeout(this.nextTick.bind(this), this.frameSpeed);
    }

    start() {

    }

    update(dt) {

    }

    onDestroy(): void {
        clearInterval(this.interval);
    }
}

b: 玩家控制类负责将玩家的操作记录,以便上传到服务端:

import BallGameData, { ballGameData } from "./GameData";
import { gameManager } from "./GameManager";
import { eventManager } from "./common/managers/eventManager";
import BaseComp from "./common/ui/baseComp";
import { angleToRand, clamp, randToAngle } from "./common/utils/util";

const { ccclass, property } = cc._decorator;

@ccclass
export default class JoyStickManager extends BaseComp {

    private Handle: cc.Sprite = null;

    /** 当前摇杆的方向 */
    private _dir: cc.Vec2 = cc.v2(0);

    private _width: number = 0;
    private _height: number = 0;

    /** 当前摇杆的角度值 */
    private _angle: number = 0;

    /** 是否在移动 */
    private isMoving: boolean = false;

    public set angle(a: number) {
        this._angle = a;
    }

    public get angle() {
        return this._angle;
    }

    /** 获得当前摇杆的方向 */
    public get dir(): cc.Vec2 {
        return this._dir;
    }

    private set dir(d: cc.Vec2) {
        this._dir = d;
    }

    __preload(): void {
        this.openFilter = true;
        super.__preload();
        gameManager.joystickManager = this;
    }

    /*** 获取客户端玩家的所有指令集 */
    getCmd(): any {
        return { dir: this.dir, angle: this.angle, moving: this.isMoving };
    }

    onLoad() {
        this.Handle.node.on(cc.Node.EventType.TOUCH_START, this.onTouchStart, this);
        this.Handle.node.on(cc.Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
        this.Handle.node.on(cc.Node.EventType.TOUCH_END, this.onTouchEnd, this);
        this.Handle.node.on(cc.Node.EventType.TOUCH_CANCEL, this.onTouchEnd, this);

        this._width = this.node.width;
        this._height = this.node.height;

    }

    private onTouchStart(event: cc.Event.EventTouch) {
        let touchPos = this.node.convertToNodeSpaceAR(event.getLocation());
        let dir = touchPos;
        this.dir = dir.normalize();
    }

    private onTouchMove(event: cc.Event.EventTouch) {
        this.isMoving = true;
        let touchPos = this.node.convertToNodeSpaceAR(event.getLocation());
        let dir = touchPos;
        this.dir = dir.normalize();

        const cocosAngle = this.dir.angle(cc.v2(0, 1));

        // v1 dot v2 > 0 同向  v1 dot v2 < 0 反向
        // v1 cross v2 > 0 同侧 v1 cross v2 < 0 反侧
        let targetAngle = randToAngle(cocosAngle);
        let resAngle = targetAngle;
        if (cc.v2(0, 1).clone().cross(this.dir) > 0) {
            // 在y轴的左侧
            // eventManager.emit("updateDir", { angle: targetAngle, dir: this.dir });
            resAngle = targetAngle;
        } else {
            // 在y轴的右侧
            // eventManager.emit("updateDir", { angle: -targetAngle, dir: this.dir });
            resAngle = -targetAngle;
        }
        this.angle = resAngle;

        const angle = this.dir.angle(cc.v2(1, 0));
        const R = this._width / 2;
        const maxX = R * Math.cos(angleToRand(this.angle + 90));
        const maxY = R * Math.sin(angleToRand(this.angle + 90));

        if (Math.abs(touchPos.x) > maxX) {
            touchPos.x = maxX;
        }

        if (Math.abs(touchPos.y) > maxY) {
            touchPos.y = maxY;
        }

        this.Handle.node.setPosition(touchPos);
    }

    private onTouchEnd(event: cc.Event.EventTouch) {
        console.log("end...");
        this.Handle.node.setPosition(cc.v2(0));
        // this.dir = cc.v2(0);
        this.isMoving = false;

        gameManager.playerManager.stop(ballGameData.selfSesssioId);
        // gameManager.networkManager.room.send("stop", { dir: { x: this.dir.x, y: this.dir.y } });
    }

    start() {

    }

    // update (dt) {}
}

c: 玩家就收到socket消息之后进行逻辑处理:

import Player from "../../server/src/rooms/entity/Player";
import { FrameDataItem } from "./GameData";
import BaseComp from "./common/ui/baseComp";

const { ccclass, property } = cc._decorator;

@ccclass
export default class BallPlayer extends BaseComp {

    private candy: cc.Sprite = null;
    private username: cc.Label = null;

    speed: number = 200;

    private _dir: cc.Vec2 = cc.v2(0);
    private _moving: boolean = false;

    public get dir() {
        return this._dir;
    }

    public set dir(d: cc.Vec2) {
        this._dir = d;
    }

    public set moving(m: boolean) {
        this._moving = m;
    }

    public get moving() {
        return this._moving;
    }

    __preload() {
        this.openFilter = true;
        super.__preload();
    }

    onLoad() {

    }

    start() {

    }

    /** 更新玩家信息 */
    updatePlayerInfo(player: Player) {
        this.moving = false;
        this.node.scale = player.scale;
        this.node.angle = player.angle;
        this.dir = cc.v2(player.dir.x, player.dir.y);

        if (cc.v2(player.x, player.y).clone().sub(cc.v2(this.node.x, this.node.y)).mag() > 5) {
            console.log("tween...");
            // tween动画
            cc.tween(this.node).to(0.05, { x: player.x, y: player.y }).start();
        } else {
            this.node.x = player.x;
            this.node.y = player.y;
        }

    }

    /** 执行服务端的帧数据 ***/
    updatePlayerFromServer(data: FrameDataItem) {
        const angle = data.angle;
        this.dir = cc.v2(data.dir.x, data.dir.y);
        this.moving = data.moving;
        this.node.angle = angle;

        if (this.moving) {
            this.node.x += this.dir.x * this.speed * (16 / 1000);
            this.node.y += this.dir.y * this.speed * (16 / 1000);
        }

    }

    update(dt) {

    }
}

 服务端房间逻辑:


import { Room, Client, Delayed } from "@colyseus/core";
import { MyRoomState } from "./schema/MyRoomState";
import { IncomingMessage } from "http";
import Player, { Vec2 } from "./entity/Player";
import { MessageType } from "./models/ServerData";

const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

/** 是否是帧同步 */
const isFrameSync: boolean = true;

/**
 * 
 * 1: mongo存储用户信息
 * 2: 进游戏之前,先登录
 * 
 */

export class MyRoom extends Room<MyRoomState> {
  maxClients = 4;
  patchRate: number = 20;

  /** 当前游戏进行的帧数 */
  public frame_index: number = 0;
  /** 帧间隔 服务端计时器 */
  public frame_interval: Delayed = null;
  /** 游戏中帧列表 */
  public frame_list: Array<Array<{ id: string, data: any }>> = [];
  /** 帧插值 服务端发送 0,3,6,9隔帧发送减少带宽压力 客户端负责补帧 */
  public frame_acc: number = 3;

  onCreate(options: any) {
    console.log("create..");
    /** 启动时钟 */
    this.clock.start();

    this.setState(new MyRoomState());
    /** 初始化游戏进行中的帧数 */
    this.resetFrameInfo();

    this.setPatchRate(20);
    if (isFrameSync) {
      this.setSimulationInterval(this.update.bind(this), 1000 / 60);
    }

    this.frame_interval = this.clock.setInterval(this._tick.bind(this), 50);
    // this.broadcastPatch();
    this.onMessage("*", this._messageHandler.bind(this));
  }

  private _tick() {
    const curFrame = this.getFrameByIndex(this.frame_index);
    this.broadcast("f", [this.frame_index, curFrame]);
    this.frame_index += this.frame_acc;
  }

  private getFrameByIndex(index: number) {
    if (!this.frame_list[index]) {
      this.frame_list[index] = [];
    }
    return this.frame_list[index];
  }

  resetFrameInfo() {
    this.frame_index = 0;
    this.frame_list = [];
  }

  generateRoomIdSingle(): string {
    let result = "";
    for (let i = 0; i < 4; i++) {
      result += LETTERS.charAt(Math.floor(Math.random() * LETTERS.length));
    }
    return result;
  }

  /**
   * 收到客户端消息
   * @param  {Client} client 客户端
   * @param  {any} type 消息类型
   * @param  {any} message 消息内容
   */
  private _messageHandler(client: Client, type: any, message: any) {
    const sessionId = client.sessionId;
    const playerId = client.id;

    console.log("sessionId is ", sessionId);
    switch (type) {
      case MessageType.MOVE:
        this.movePlayer(playerId, message);
        break;
      case MessageType.STOP:
        this.stopPlayer(playerId);
        break;
      case MessageType.LINK:
        this.state.currentFrame = message.frame;
        break;
      case MessageType.CMD:
        this.onCmd(playerId, message);
        break;
      case MessageType.ALLFRAMES:
        this.onGetAllFrames(client, message);
        break;
      default:
        break;
    }
  }

  /**
   * 获得服务器上的所有游戏帧,并且发给客户端 让客户端补帧
   * @param  {Client} client 客户端
   * @param  {any} message 消息内容
   */
  onGetAllFrames(client: Client, message: any) {
    let frames: any = [];
    for (let i = 0, len = this.frame_list.length; i < len; i++) {
      if (this.frame_list[i]) {
        frames.push([i, this.frame_list[i]]);
      }
    }
    if (this.frame_list.length == 0) {
      frames.push([0, []]);
    }

    this.send(client, MessageType.ALLFRAMES, frames);
  }

  /**
   * 客户端发过来的指令
   * @param  {string} id 客户端的id
   * @param  {any} data 指令内容
   */
  onCmd(id: string, data: any) {
    console.log(`客户端id: ${id}: 操作指令:`, data);
    // 存在同一帧情况下,多个客户端同时发送指令的情况
    this.pushFrameList({ id, data });
  }


  /**
   * 向帧列表中添加数据
   * @param  {{id:string,data: any}} data
   */
  private pushFrameList(data: { id: string, data: any }) {
    if (!this.frame_list[this.frame_index]) {
      this.frame_list[this.frame_index] = [];
    }
    const selfIsCurrentFrame = this.frame_list[this.frame_index].find(item => item.id == data.id);
    if (!selfIsCurrentFrame)
      this.frame_list[this.frame_index].push(data);
  }

  movePlayer(id: string, data: { dir: { x: number, y: number }, angle: number, pos: { x: number, y: number } }) {
    const player = this.state.playerMap.get(id);
    if (!player) return;

    // 客户端传进来的方向向量
    player.dir = new Vec2(data.dir.x, data.dir.y);
    player.angle = data.angle;
    player.isMoving = true;
  }

  stopPlayer(id: string) {
    const player = this.state.playerMap.get(id);
    if (!player) return;

    player.isMoving = false;
  }

  onAuth(client: Client<any>, options: any, request?: IncomingMessage) {
    console.log("onAuth");
    return true;
  }

  onJoin(client: Client, options: any) {
    console.log(client.sessionId, "joined!");

    this.send(client, "joinSuccess", client.id);
    // 存储用户
    this.state.playerMap.set(client.id, new Player(client.id));
  }

  onLeave(client: Client, consented: boolean) {
    console.log(client.sessionId, "left!");
    this.send(client, "bye", { id: client.id });
  }

  onDispose() {
    console.log("room", this.roomId, "disposing...");
    this.frame_interval.clear();
    this.resetFrameInfo();
  }

  update(dt: number) {
    if (!this.state) return;
    if (this.state.playerMap && this.state.playerMap.size === 0) return;

    const interval = dt / 1000;
    for (let player of this.state.playerMap.values()) {
      player.update(interval);
    }
  }
}

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

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

相关文章

chatgpt赋能python:Python计算字符串中的小写字母

Python计算字符串中的小写字母 在搜索引擎优化中&#xff0c;我们经常需要计算一个文本中小写字母的数量。Python语言的强大和灵活性使得它成为实现这一目标的理想选择。在本文中&#xff0c;我们将介绍如何使用Python语言计算字符串中的小写字母。 了解Python字符串 在Pyth…

Nodejs三、模块化

零、文章目录 Nodejs三、模块化 1、模块化的基本概念 &#xff08;1&#xff09;模块化是什么 模块化是指解决一个复杂问题时&#xff0c;自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说&#xff0c;模块是可组合、分解和更换的单元。 &#xff08;2&#xff…

基于Java汽车养护管理系统设计实现(源码+lw+部署文档+讲解等)

博主介绍&#xff1a; ✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战 ✌ &#x1f345; 文末获取源码联系 &#x1f345; &#x1f447;&#x1f3fb; 精…

计算机网络408题(2020-2022)

2020年 2021年 下面借此大题来分析主机H1访问Web服务器的应用场景的全部过程。 2022年 综合大题 冲突域是指一组设备共享同一段物理网络&#xff0c;当其中一个设备发送数据时&#xff0c;其他设备必须等待&#xff0c;以避免数据冲突。在一个冲突域中&#xff0c;当两个或多…

计算机网络管理 实验4(二) SNMP报文管理信息结构SMI及其规定的ASN.1分析并使用Wireshark抓包分析sysContact的相关信息

⬜⬜⬜ &#x1f430;&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;(*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;&#x1f430;⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &am…

关系数据库SQL数据查询

关系数据库SQL数据查询 数据查询 一、单表查询 1.查询仅涉及一个表&#xff0c;选择表中的若干列 [例1] 查询全体学生的学号与姓名。SELECT Sno,SnameFROM Student; [例2] 查询全体学生的姓名、学号、所在系。SELECT Sname,Sno,SdeptFROM Student;查询全部列 选出所有属…

python代码实现生成二维码

二维码结构&#xff1a; 从图中我们可以看出二维码结构整体划分为功能图形和编码区两大部分&#xff0c;功能图形又细分为&#xff1a;空白区、位置探测图形、位置探测图形分隔符、定位图形、校正图形&#xff0c;而编码区细分为&#xff1a;格式信息、版本信息、数据和纠错码字…

Flutter进阶-List数组的深浅拷贝问题

浅拷贝&#xff1a;拷贝指针深拷贝&#xff1a;拷贝一份新的对象 浅拷贝出现的问题&#xff1a;操作一个数组另一个数组也发生改变 List list [1, 2, 3, 4, 5]; List copyList list; copyList[0] abc; print(list);///打印&#xff1a; [abc,2,3,4,5] print(copyList);///打…

机器学习——博客推荐系统

前言 在当今的信息时代&#xff0c;技术博客已成为知识、见解和娱乐的重要来源。随着博客内容的日益丰富&#xff0c;找到最相关和最引人入胜的文章对用户来说可能是一项艰巨的任务。为了应对这一挑战&#xff0c;我们需要一个全面的博客推荐系统&#xff0c;利用尖端技术和机…

RabbitMQ + SpringCloud使用及避坑(大章)

RabbitMQ 的开发语言是Erlang&#xff0c;它的源码阅读起来学习成本太高了&#xff0c;所以这里就不详细看了&#xff0c;本次主要是结合springCloud 的项目来真正使用RabbitMQ 的几种交换器&#xff0c;还有一些业务场景的模拟&#xff0c;最主要的还是避坑。 为什么说是避坑…

JavaSE进阶--玩转IO流

文章目录 前言一、File类介绍1、概念引入2、实际应用2.1 操作文件2.2 操作文件夹 二、IO流介绍三、字符流1、读文件1.1 一次读一个1.2 一次读多个&#xff0c;使用char数组去装 2、写文件2.1 一次写一个2.2 一次写完&#xff0c;使用字符数组 3、文件复制3.1 综合应用3.2 使用缓…

ESP32(MicroPython)LVGL输入字符显示到OLED屏幕

本程序用于输入字符显示到OLED屏幕&#xff08;按回车键输出字符到屏幕&#xff09;&#xff0c;依次输出六行字符&#xff0c;再按回车会清空屏幕并从第一行开始输出。 代码如下 import lvgl as lv import time from espidf import VSPI_HOST from ili9XXX import ili9341…

模拟随机验证码

网拷一篇中英文文本&#xff0c;去除标点空格作为验证码字符集。 (本笔记适合对 python 的随机模块有一定了解的的 coder 翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.org/ Free&#xff1a;大咖免费“圣经”教程《 python 完全自学教程》&a…

如何在IDEA上实现JDBC编程

&#x1f495;人生在世&#xff0c;不如意事常八九&#xff0c;若把烦恼全都写在纸上&#xff0c;时间长了纸也会烦。所以&#xff0c;没事的时候给自己多一些微笑&#xff0c;少一些烦恼。&#x1f495; &#x1f43c;作者&#xff1a;不能再留遗憾了&#x1f43c; &#x1f3…

【备战秋招】每日一题:4月23日美团春招第二题:题面+题目思路 + C++/python/js/Go/java带注释

为了更好的阅读体检&#xff0c;为了更好的阅读体检&#xff0c;&#xff0c;可以查看我的算法学习博客第二题-制作骰子 在线评测链接:P1246 题目内容 塔子哥是一个喜欢手工制作的人&#xff0c;他经常用各种材料制作一些有趣的物品。他最近想要制作一个骰子&#xff0c;但是…

Oracle11gRAC安装JVM组件

目录&#xff1a; 安装前检查&#xff1a;安装JVM组件&#xff1a;方式一&#xff08;图形化安装&#xff09;方式二&#xff08;执行脚本安装&#xff09; 检查DB JVM状态脚本 安装前检查&#xff1a; 检查角色&#xff1a; select * from dba_roles where ROLE in (JAVAIDP…

分布式安装配置spark-3.2.3

Spark是一个基于内存的大数据计算框架&#xff0c;可以与Hadoop集成&#xff0c;提供更快速的数据处理能力。本文将介绍如何在三个Ubuntu系统上搭建一个Spark集群。 主要步骤包括&#xff1a; 准备工作&#xff1a;下载安装包&#xff0c;设置环境变量&#xff0c;解压安装包…

基于Elasticsearch与Hbase组合框架的大数据搜索引擎

本项目为学校大数据工程实训项目&#xff0c;共开发4周&#xff0c;答辩成绩不错。代码仓库放文章尾&#xff0c;写的不好&#xff0c;代码仅供参考。 搜索 对于结构化数据&#xff0c;因为它们具有特定的结构&#xff0c;所以我们一般都是可以通过关系型数据库&#xff08;M…

【读书笔记】《房思琪的初恋乐园》- 林奕含

文章目录 第一章 乐园第二章 失乐园 第一章 乐园 钱爷爷说&#xff1a;“两个小美女有心事啊&#xff1f;”怡婷最恨人家叫她们两个小美女&#xff0c;她很这种算术上的好心。 在外人看来&#xff0c;女生无论长得漂亮还是长得不怎么样都是一种原罪。或者正如上野千鹤子所说那样…

pocketgl

pocketgl支持在自己的web页面集成类似 Shadertoy、 Threejs 等基于webGL 的渲染图形窗口&#xff0c; 并且拥有shader代码高亮编辑器支持实时修改和预览。 其自带的mesh包含两个Sphere 和 Teaport, 同时支持上床自定义的网格 和 背景天空盒。其既支持像Shadertoy 这种只包含fra…