JavaScript互动游戏开发教程:鸟游戏

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:【鸟游戏】是一款使用JavaScript开发的简单有趣且具有挑战性的互动游戏,玩家需控制一只鸟儿避开不断上升的管道。游戏利用JavaScript的事件驱动和定时器功能,实现动态的游戏循环。本文涵盖游戏对象定义、动画制作、用户输入处理、碰撞检测、计分系统、游戏状态管理、性能优化和DOM操作等关键游戏开发技术。读者可以参考【Bird-game-main】代码库深入学习JavaScript编程和游戏开发的基础。

1. JavaScript游戏对象设计

1.1 游戏对象的概念和重要性

在JavaScript中,游戏对象是构建游戏的基石。它们代表了游戏世界中的实体,如角色、道具、敌人等。理解如何设计这些对象至关重要,因为这将影响游戏的可维护性、扩展性和性能。游戏对象通常包含属性和方法,其中属性定义对象的特征,方法定义对象的行为。

1.2 基于原型的继承与封装

JavaScript是一种基于原型的语言,这意味着我们可以使用原型链来实现继承。这种继承方式在游戏开发中非常有用,因为它允许我们创建灵活的对象结构,并共享功能。封装则是指将数据和操作数据的方法绑定在一起,形成一个独立的对象,这是面向对象编程的核心概念之一。

1.3 设计模式在游戏对象中的应用

设计模式提供了一种可重用的解决方案,用于解决软件开发中常见的问题。在JavaScript游戏对象设计中,常用的模式包括工厂模式、单例模式、装饰模式和观察者模式。这些模式可以帮助我们构建更加清晰、可维护和可扩展的游戏代码。

// 一个简单的JavaScript游戏对象示例
function GameCharacter(name, type) {
    this.name = name;
    this.type = type;
    this.health = 100;
}

GameCharacter.prototype.getIntro = function() {
    return "I am a " + this.type + " game character named " + this.name + ".";
};

// 创建游戏角色实例
var hero = new GameCharacter('Hero', 'Heroic');
console.log(hero.getIntro());

这个例子展示了一个游戏角色对象的设计,使用构造函数和原型方法创建了一个可重用的游戏角色对象。通过这样的设计,我们可以轻松地扩展更多功能,同时保持代码的整洁和结构化。

2. 动态游戏循环的实现

游戏循环是游戏开发的核心组件之一,它负责不断更新游戏状态,并将结果绘制到屏幕上。游戏循环的效率直接影响着游戏的流畅度和玩家的体验。本章节将深入探讨游戏循环的实现方法、性能优化和相关技术细节。

2.1 游戏循环基础

2.1.1 游戏循环的定义和作用

游戏循环,顾名思义,是指游戏运行时不断重复的一系列操作,其主要作用是更新游戏状态,并渲染新的画面。一个基本的游戏循环包括输入处理、状态更新和渲染输出三个部分。在执行这三部分时,必须保证游戏的循环运行得足够平滑,即帧率要保持稳定,以避免给玩家带来卡顿感。

游戏循环的实现方式根据平台和需求的不同有很多种,但其核心理念是相通的。

2.1.2 游戏循环的常见模式(requestAnimationFrame、setTimeout等)

在Web开发中,常见的游戏循环模式包括使用 setInterval setTimeout ,以及更现代的 requestAnimationFrame

  • setInterval 方法可以定时执行代码,但它不考虑浏览器的重绘时间,可能会导致游戏状态更新与画面渲染不同步。
  • setTimeout 可以实现与 setInterval 类似的效果,但它是基于回调而非周期性执行。其优势在于能够根据回调的执行情况动态调整下一次执行的时间,从而更好地处理性能问题。
  • requestAnimationFrame 是专为动画设计的,它会在浏览器重绘前调用指定的函数,保证更新和渲染在正确的时间发生,从而提供更流畅的动画效果,并减少CPU和GPU的负载。
// 使用requestAnimationFrame实现游戏循环
function gameLoop(timestamp) {
  updateGame();
  renderGame();
  requestAnimationFrame(gameLoop);
}

requestAnimationFrame(gameLoop);

2.2 游戏循环的优化

随着游戏复杂度的增加,游戏循环的性能优化变得尤为重要。游戏循环的优化主要关注帧率控制、卡顿消除和渲染效率提升。

2.2.1 游戏帧率与性能监控

帧率(Frame Rate)通常以每秒帧数(Frames Per Second, FPS)来衡量,它直接影响游戏的流畅度。在JavaScript中可以通过记录时间差来简单计算当前的帧率。

// 简单的帧率计算和性能监控
const frameRateStats = {
  lastTime: performance.now(),
  frames: 0
};

function updateGame() {
  const currentTime = performance.now();
  const timeDelta = currentTime - frameRateStats.lastTime;
  if (timeDelta > 1000) {
    const fps = (frameRateStats.frames / (timeDelta / 1000)).toFixed(1);
    console.log(`FPS: ${fps}`);
    frameRateStats.frames = 0;
    frameRateStats.lastTime = currentTime;
  } else {
    frameRateStats.frames++;
  }
  // 更新游戏逻辑
}
2.2.2 减少卡顿和提升流畅度的策略

要减少卡顿并提升游戏流畅度,关键是要优化游戏循环中的更新和渲染过程:

  • 避免复杂的计算 :在游戏循环中执行复杂的计算会直接拖慢帧率,应当尽量避免或移到循环之外。
  • 按需渲染 :只有在必要时才重新渲染游戏画面,可以通过检测对象的位置和状态变化来决定是否需要渲染。
  • 使用Web Workers :将计算密集的任务放在Web Workers中执行,可以避免阻塞主游戏循环。
  • 使用requestAnimationFrame :确保渲染与浏览器的重绘时间同步,利用它的内置节流机制优化性能。
// 使用Web Worker处理计算密集型任务
const worker = new Worker('worker.js');

worker.postMessage('start');

worker.onmessage = function(event) {
  if (event.data.type === 'update') {
    updateGame(event.data.payload);
  }
};

function updateGame(data) {
  // 更新游戏逻辑
  // ...
  requestAnimationFrame(gameLoop);
}

通过以上优化,游戏循环的性能可以得到显著提升,从而为玩家提供更好的游戏体验。在下一章节中,我们将探讨如何处理用户的输入事件,进一步增强游戏的交互性和响应性。

3. 用户输入响应机制

用户输入是任何交互式游戏的核心,它直接影响玩家的游戏体验。响应机制必须精确、快速且易于管理。本章将探讨输入事件的基础知识,以及如何实现高级输入管理。

3.1 输入事件基础

3.1.1 鼠标与键盘事件的监听和处理

在JavaScript中处理鼠标和键盘事件涉及监听这些事件并做出相应的响应。 addEventListener 是实现这一目标的标准方法。

// 添加键盘事件监听器
document.addEventListener('keydown', function(event) {
    // 键盘按下时的操作
});

// 添加鼠标事件监听器
document.addEventListener('mousemove', function(event) {
    // 鼠标移动时的操作
});

上述代码展示了如何添加键盘和鼠标的事件监听器。 keydown 用于检测键盘按下, mousemove 用于检测鼠标移动。在每个事件处理函数中, event 参数包含了许多关于发生的事件的信息,如哪个键被按下或鼠标指针的确切位置。

代码逻辑逐行解读:

  • document.addEventListener('keydown', function(event) {...}); :向 document 对象添加一个键盘按下事件的监听器。
  • document.addEventListener('mousemove', function(event) {...}); :向 document 对象添加一个鼠标移动事件的监听器。

3.1.2 触摸事件在移动设备上的应用

触摸事件特别重要于移动设备,因为它们没有鼠标或键盘。触摸事件包括 touchstart touchmove touchend touchcancel ,它们允许开发者创建响应用户触摸操作的交互。

document.addEventListener('touchstart', function(event) {
    // 处理触摸开始时的逻辑
}, false);

触摸事件对象 event 与鼠标事件不同,它包含了关于触摸点的信息。处理触摸事件时,通常需要检查 touches targetTouches changedTouches 属性来获取相关的触摸点数据。

代码逻辑逐行解读:

  • document.addEventListener('touchstart', function(event) {...}); :向 document 对象添加一个触摸开始事件的监听器。

3.2 高级输入管理

3.2.1 输入防抖和输入预测

在复杂的游戏环境中,输入事件可能频繁发生,导致性能问题。输入防抖(debouncing)和输入预测(throttling)技术能够减少事件处理函数的调用频率,从而优化性能。

防抖技术通过在事件停止触发后再执行函数来避免频繁调用。输入预测则限制了函数在给定时间内只能被调用一次。

function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// 使用防抖技术处理事件
window.addEventListener('resize', debounce(function() {
    // 在窗口大小调整完成后执行的操作
}, 1000));

代码逻辑逐行解读:

  • function debounce(func, wait) {...} :创建一个防抖函数, func 是需要执行的函数, wait 是延迟时间。
  • return function executedFunction(...args) {...} :返回一个新的函数,这个新函数会接收任意参数,然后传递给 func 函数。
  • clearTimeout(timeout) :如果新的事件触发,清除之前的计时器。
  • timeout = setTimeout(later, wait); :设置一个新的计时器,等待指定的时间后执行 later 函数。

3.2.2 游戏手柄和多点触控支持

现代游戏支持多样的输入设备,例如游戏手柄和多点触控屏。要使游戏能够响应这些输入,开发者必须首先检测设备类型,然后应用合适的事件监听器。

if ('ongamepadconnected' in window) {
    window.addEventListener('gamepadconnected', function(e) {
        // 处理游戏手柄连接事件
    });
}

// 多点触控事件监听器
document.addEventListener('touchstart', function(event) {
    if (event.touches.length > 1) {
        // 处理多点触控事件
    }
});

代码逻辑逐行解读:

  • if ('ongamepadconnected' in window) {...} :检查当前浏览器是否支持游戏手柄事件。
  • window.addEventListener('gamepadconnected', function(e) {...}); :添加游戏手柄连接事件的监听器。

3.3 输入事件处理与优化

处理输入事件并不仅仅是添加事件监听器那么简单。为了确保游戏响应迅速且无卡顿,必须对事件处理逻辑进行优化。以下是一些关键的优化策略:

  1. 避免全局事件监听器 :在大型游戏应用中,避免将事件监听器绑定到 document window 对象上。相反,应尽可能地将监听器绑定到那些实际需要接收事件的元素上。

  2. 事件委托 :事件委托是一种利用事件冒泡机制的技术,将事件监听器放置在父元素上,而不是在每一个子元素上。这样可以减少事件监听器的数量,提高性能。

  3. 使用 requestAnimationFrame :对于需要动画效果的输入,可以使用 requestAnimationFrame 来代替 setTimeout setInterval requestAnimationFrame 会在浏览器进行重绘前调用指定的函数,使得动画更加平滑。

3.4 实现输入响应机制的最佳实践

一个高效的输入响应机制需要遵循几个最佳实践:

  1. 事件最小化 :确保只有相关事件被处理,避免无谓的计算和资源消耗。

  2. 异步处理 :为了不阻塞主线程,将耗时的输入处理任务放在Web Workers中异步执行。

  3. 性能测试 :定期进行性能测试,确保输入响应机制不会对游戏性能造成负面影响。

通过以上讨论,我们可以看到,用户输入响应机制是游戏开发中一个复杂但至关重要的部分。游戏开发者必须利用各种技术手段来确保输入处理既准确又高效。

4. 碰撞检测技术

4.1 碰撞检测基础

碰撞检测是游戏中非常核心的交互技术之一,它涉及到游戏对象的物理交互处理。在物理世界中,两个物体只有在它们的边界相交时才发生碰撞。在计算机游戏中,我们需要通过算法来近似这一过程。碰撞检测的技术难点在于如何在保证检测精度的同时,尽可能减少计算资源的消耗。

4.1.1 碰撞检测的基本原理和算法

碰撞检测的算法通常分为两类:粗略检测和精细检测。

  • 粗略检测 通常使用轴对齐包围盒(Axis-Aligned Bounding Box, AABB)或边界球(Bounding Sphere)作为代理来快速剔除不会发生碰撞的对象。这种检测非常快速但并不精确,因此适用于场景中对象数量较多,且不需要高精度碰撞反馈的情况。

  • 精细检测 则更加精确地确定两个对象是否真正接触,常见的方式包括包围球、凸包(Convex Hull)和多边形相交测试等。这类检测方法提供了更真实的碰撞反馈,适用于需要精确碰撞检测的场景,如赛车、格斗类游戏。

下面是一个使用AABB进行碰撞检测的简单示例代码块,该示例展示了如何判断两个矩形是否有交集:

function isAABBIntersection(rect1, rect2) {
  // rect1 和 rect2 是具有 {x, y, width, height} 属性的对象
  if (rect1.x < rect2.x + rect2.width &&
      rect1.x + rect1.width > rect2.x &&
      rect1.y < rect2.y + rect2.height &&
      rect1.y + rect1.height > rect2.y) {
    return true;
  }
  return false;
}
4.1.2 碰撞体积的简化与优化

为了提高碰撞检测的性能,经常会使用各种方法来简化碰撞体积。在三维空间中,可以使用球体、盒子、胶囊或其他形状来近似对象的体积。这些形状可以通过数学公式快速计算出它们在空间中的位置和相互关系。

为了进一步优化性能,可以引入分层或空间分割技术,如四叉树(Quadtree)、八叉树(Octree)或者格子(Grid)。这些结构可以帮助快速确定潜在的碰撞候选者,从而避免对整个场景中所有对象进行检测。

// 示例:使用四叉树进行粗略碰撞检测
class Quadtree {
  // 代码省略具体实现细节,包括初始化、区域划分、插入和检索等
}

// 当需要检测碰撞时
const quadTree = new Quadtree(...);
quadTree.insertAll(gameObjects); // 将游戏对象全部加入四叉树
const candidates = quadTree.retrieve(potentialCollider); // 获取可能与潜在碰撞对象发生交互的对象集合
// 对这些候选对象执行精确的碰撞检测算法

4.2 碰撞检测的应用

碰撞检测被广泛应用于游戏中的多种交互中,不同游戏对象之间可能发生的碰撞有其特定的处理逻辑。此外,基于物理引擎的碰撞响应可以给游戏增加真实感,提升玩家的游戏体验。

4.2.1 不同游戏对象之间的碰撞逻辑

在开发一款游戏时,开发者需要考虑不同游戏对象之间的交互逻辑。比如,玩家控制的角色可能需要和敌方角色、道具、环境等发生交互,每种交互都可能涉及到碰撞检测。例如,在角色移动过程中,必须检测其是否与障碍物发生碰撞,并做出相应的反馈,如停止移动或反弹等。

此外,还有更复杂的碰撞逻辑,比如角色跳跃时检测是否踩到了移动平台,或者是否可以触发隐藏的机关等。对于这些情况,开发者需要编写额外的代码逻辑,以处理这些复杂的游戏机制。

4.2.2 基于物理引擎的碰撞响应

现代游戏开发中,物理引擎如Matter.js、Box2D或Phaser Physics已经变得非常流行。这些物理引擎提供了一套完整的解决方案来处理碰撞检测和物理模拟。开发者只需要定义物体的形状、质量、摩擦力等属性,物理引擎就会自动处理碰撞检测和物理响应。

在物理引擎中,开发者可以设置各种物理材料的属性,如弹性和摩擦力,并为不同类型的碰撞事件编写回调函数。这样可以实现更加丰富和动态的交互体验,而无需从头开始计算和处理每一项物理运动。

// 示例:使用Phaser Physics引擎进行碰撞响应
gameScene.physics.add.collider(player, platforms, function(player, platform) {
  // 当玩家与平台发生碰撞时的逻辑处理
});

通过这些基础和应用层面的介绍,我们可以看出碰撞检测技术在游戏开发中的重要性以及实现方式的多样性。掌握这些技术不仅可以优化游戏的交互体验,还可以大幅提升游戏的性能和表现力。

5. 计分系统构建

计分系统是游戏设计中不可或缺的一部分,它不仅能够追踪玩家的表现,还能激发玩家继续游玩并超越自己的欲望。本章节将详细介绍计分系统的设计与实现,包括计分逻辑的基础构建,以及与用户交互的界面设计。

5.1 计分逻辑基础

5.1.1 游戏得分的计算方法

游戏得分的计算可以相当简单,也可以非常复杂,取决于游戏设计的需要。基本的得分计算方法通常基于玩家完成的任务,例如击败敌人、收集物品或完成关卡。对于一些复杂的游戏,得分系统可能涉及到多维度的评分,比如时间、准确率、连击数等。

对于计分逻辑的实现,首先我们需要定义得分的规则:

// 示例:一个简单的得分函数
function calculateScore(pointsEarned, bonusPoints, penalties) {
    let totalScore = pointsEarned; // 初始得分
    totalScore += bonusPoints; // 加上奖励分
    totalScore -= penalties; // 减去扣分项
    return totalScore > 0 ? totalScore : 0; // 确保得分不为负
}

其中 pointsEarned 代表玩家根据完成的动作应获得的基本分, bonusPoints 代表额外奖励分数, penalties 代表扣分项。游戏设计者可以根据具体的游戏逻辑来调整这些参数。

5.1.2 系统内部数据结构设计

为了能够有效地管理和更新玩家的得分,我们需要在系统内部设计合适的数据结构。通常情况下,玩家的得分可以存储在一个对象中,该对象包含玩家ID作为键,得分作为值。

// 示例:得分存储对象
let playerScores = {
    'player1': 150,
    'player2': 200,
    'player3': 120
};

当玩家完成一个得分动作时,我们会更新这个对象。使用对象来存储玩家得分的好处是读写速度快,适合频繁的得分更新操作。

5.2 计分界面与用户交互

5.2.1 计分板的动态更新

计分板是玩家可以看到自己得分的地方。为了实现计分板的动态更新,我们需要确保计分系统能够实时反映得分变化。这通常意味着每当玩家得分更新时,计分板的相应部分也会刷新显示最新的分数。

// 示例:更新计分板函数
function updateScoreboard(playerId, newScore) {
    const scoreboardElement = document.getElementById('playerScore' + playerId);
    scoreboardElement.textContent = newScore;
}

这里假设计分板是HTML页面中的元素,其ID与玩家ID相关联。每次调用 updateScoreboard 函数时,对应玩家的分数就会在页面上更新。

5.2.2 用户奖励与排行榜机制

为了提高玩家的参与度,游戏中的奖励机制非常重要。根据得分高低,玩家可以获得相应的奖励,比如虚拟货币、游戏内物品或其他特权。设计一个排行榜系统,根据玩家的得分来进行排名,能进一步提高玩家的竞争意识。

排行榜需要定时更新,展示前N名玩家的得分情况。这可能需要将所有玩家的得分数据进行排序,并且只展示得分在前N名的玩家信息。

// 示例:构建一个简单的排行榜
function buildLeaderboard(playerScores, topN) {
    let leaderboard = Object.keys(playerScores)
        .map(playerId => ({ id: playerId, score: playerScores[playerId] }))
        .sort((a, b) => b.score - a.score) // 降序排序
        .slice(0, topN); // 取前N名

    // 显示排行榜数据
    leaderboard.forEach((entry, index) => {
        console.log(`${index + 1}. ${entry.id}: ${entry.score}`);
    });
}

在这个例子中,我们通过映射玩家ID到它们的得分来创建一个对象数组,然后按得分降序排序,并且只取前N名玩家来构建排行榜。这个排行榜可以在游戏内的特定界面展示,也可以通过排行榜API接口提供给外部用户查看。

通过对计分逻辑的基础构建和计分界面与用户交互的深入探讨,我们能够看到一个精心设计的计分系统是如何增强游戏体验的。在下一章节中,我们将探讨游戏状态管理的方法,这是维护游戏运行的另一个核心要素。

6. 游戏状态管理方法

在复杂的游戏开发中,状态管理是保持游戏逻辑清晰和高效执行的关键。良好的状态管理能确保游戏在不同场景下正确响应,同时方便实现游戏的保存和加载功能。本章节将探讨如何设计和实现一个游戏状态机以及状态的持久化存储机制。

6.1 状态机的设计与实现

状态机是游戏开发中常见的管理状态的模式,能够帮助我们清晰地定义游戏在不同阶段的行为。

6.1.1 游戏状态转换的逻辑

一个游戏通常包含多种状态,比如开始菜单、游戏中、游戏暂停、游戏结束等。状态机通过定义状态转换逻辑,来处理用户输入或游戏内部事件导致的状态变迁。

// 一个简单的状态机实现示例
const StateMachine = {
  // 状态
  states: {
    menu: 'menu',
    playing: 'playing',
    paused: 'paused',
    gameover: 'gameover'
  },
  // 初始状态
  currentState: 'menu',
  // 状态转换方法
  changeState: function(newState) {
    this.currentState = newState;
    console.log(`Game state changed to ${newState}`);
  },
  // 根据当前状态和输入处理状态转换
  handleInput: function(input) {
    switch(this.currentState) {
      case 'menu':
        if (input === 'startGame') this.changeState('playing');
        break;
      case 'playing':
        if (input === 'pauseGame') this.changeState('paused');
        if (input === 'gameOver') this.changeState('gameover');
        break;
      case 'paused':
        if (input === 'resumeGame') this.changeState('playing');
        break;
    }
  }
};

6.1.2 状态机在游戏逻辑中的应用

在实际游戏逻辑中,状态机可以被用来控制游戏的主流程。通过设置不同的状态,游戏可以在玩家与游戏互动时,做出正确的响应。

// 使用状态机控制游戏流程
function gameLoop(input) {
  StateMachine.handleInput(input);
  switch(StateMachine.currentState) {
    case 'menu':
      // 显示菜单逻辑
      break;
    case 'playing':
      // 游戏主循环逻辑
      break;
    case 'paused':
      // 游戏暂停逻辑
      break;
    case 'gameover':
      // 游戏结束逻辑,可能包含重新开始或退出
      break;
  }
}

6.2 游戏状态的保存与加载

除了处理游戏当前的状态,游戏的保存和加载机制也非常重要。这允许玩家在任意时刻保存他们的进度,并在需要时能够恢复。

6.2.1 游戏进度的持久化存储

游戏进度的持久化存储通常采用本地存储(localStorage)或IndexedDB。这些技术可以跨会话保存游戏状态。

// 使用localStorage保存游戏状态示例
function saveGameState() {
  // 假设我们有一个表示当前游戏状态的对象
  const gameState = {
    level: 5,
    score: 250,
    playerHealth: 50
  };
  // 将游戏状态转换为字符串并存储
  localStorage.setItem('gameState', JSON.stringify(gameState));
}

// 加载游戏状态
function loadGameState() {
  const gameState = JSON.parse(localStorage.getItem('gameState'));
  // 游戏加载时的状态恢复逻辑
  console.log('Loaded game state:', gameState);
}

6.2.2 游戏存档与快速恢复功能

提供游戏存档功能可以让玩家保存当前的游戏状态,并能够快速地从这个状态继续游戏。这通常涉及到处理文件输入输出,或者使用现代Web API来实现。

// 使用IndexedDB保存复杂游戏状态的示例
function saveGameStateToIndexedDB() {
  const request = indexedDB.open('gameDB', 1);
  request.onerror = function(event) {
    console.log('Error saving game state: ', event.target.errorCode);
  };
  request.onupgradeneeded = function(event) {
    const db = event.target.result;
    db.createObjectStore('gameStates', {keyPath: 'id'});
  };
  request.onsuccess = function(event) {
    const db = event.target.result;
    const transaction = db.transaction('gameStates', 'readwrite');
    const store = transaction.objectStore('gameStates');
    const gameState = {
      id: 'current', // 假设这是我们要存储的游戏状态ID
      level: 5,
      score: 250,
      playerHealth: 50
    };
    store.put(gameState);
    transaction.oncomplete = function() {
      console.log('Game state saved to IndexedDB');
    };
  };
}

在下一章节中,我们将探讨性能优化的基础,以及如何识别和处理性能瓶颈,避免内存泄漏和循环引用。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:【鸟游戏】是一款使用JavaScript开发的简单有趣且具有挑战性的互动游戏,玩家需控制一只鸟儿避开不断上升的管道。游戏利用JavaScript的事件驱动和定时器功能,实现动态的游戏循环。本文涵盖游戏对象定义、动画制作、用户输入处理、碰撞检测、计分系统、游戏状态管理、性能优化和DOM操作等关键游戏开发技术。读者可以参考【Bird-game-main】代码库深入学习JavaScript编程和游戏开发的基础。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值