Javascript项目— 甜心教主《爱你》版 贪吃蛇游戏

0.项目展示

 

目录

0.项目展示

一,贪吃蛇游戏

1. 初始化及引入Game类

2.页面的初始化

3. 蛇类的初始化

4.蛇的运动更新与渲染

4.1 清屏操作

 4.2 蛇的运动更新 

基于以上,我们对运动模块进行补全: 

 4.3 蛇的渲染  

 4.4 蛇的相关方法的调用(清屏--更新--渲染)

 5.键盘监听事件

5.1 将定时器事件绑定在Game原型上 

         5.2 键盘监听事件方法的书写

​ 5.3 运动的合理性规划——解决原地调头的问题

 6.判定游戏中蛇死亡——游戏结束的方式

​7.关于蛇的食物的创建 

       7.1 创建一个 Food类,用来产生食物

        7.2 随机产生食物的初始化设置——食物不能出现在蛇的身上

 8 蛇吃食物 & 蛇运动速度的更新

8.1 蛇吃食物——蛇身变长

 8.2 对 被吃掉的食物 的清除操作

8.3 蛇运动速度的加快 

 8.4 分数的增加

​ 9.完整代码

9.1 Game.js部分

 9.2 Snake.js 部分

9.3 Food.js 部分

9.4 html 部分


一,贪吃蛇游戏

介绍:这里采用模块化编程,即外联的方式进行编程,通过对类的拆分,单独用一个js文件表示每个类, 而不是所有部分都写在HTML中,Game类作为中介类。

1. 初始化及引入Game类

这里先简单的引入这个类,并进行测试

JS文件

2.页面的初始化

进行页面的初始化,对页面的初始化布局不是直接写各种的html标签,而是通过 Game 进行的初始化并追加节点上树。具体过程如下

//JS文件内容

function Game(){
 //  alert('欢迎进入贪吃蛇游戏')

   //设置行数和列数
   this.row=30
   this.cal=30

   //初始化
   this.Init()
}

//1.在对象的原型上进行初始化方法的书写
Game.prototype.Init=function(){

   //1.1 创建一个 table 标签元素  表单
   this.dom=document.createElement("table")

   var tr,td
   //1.2 遍历行和列
   for(var i=0;i<this.row;i++){
      //1.2.1 遍历行,创建节点上树    行
      tr=document.createElement("tr")

      for(var j=0;j<this.cal;j++){
      //1.2.2 遍历列,创建节点上树    单元格   追加到 tr 中,作为 tr 的子元素
         td=document.createElement("td")

      //1.2.3 追加节点上树   使每一行都有 20 个单元格
         tr.appendChild(td)        
      }

      //1.2.4 插入节点tr,其 父节点为 this.dom,即 table 中的一个子元素
      this.dom.appendChild(tr)
   }

   //1.3 表格上树,即插入 table 至 app中
   //获取 id 为 app 的元素,并使 table 上树,即 div 中加入一个子元素 table
   //此时可以发现 table 有 20 个 tr,即 20行
   document.getElementById("app").appendChild(this.dom)
}
//html 文件内容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪吃蛇游戏</title>

    <style>
        table{
            /* border-collapse属性,属性值collapse可以使边框合并,默认值sparate边框分开 */
            border-collapse: collapse;
            border: 1px solid #aaa;
        }

        td{
            width: 20px;
            height: 20px;
            border: 1px solid #aaa;
        }
    </style>
</head>
<body>
    <div id="app"></div>

    <!-- 引入js文件 -->
    <script src="1.贪吃蛇.js"></script>
    <script>
        new Game()
    </script>
</body>
</html>

结果显示,表格初始化完成

3. 蛇类的初始化

        再创建一个 js 文件,用来进行对 蛇类 的初始化及方法的书写。先初始化身体,在 Game.js 文件中对 蛇类 进行实例化,有利于相关方法的调用。

4.蛇的运动更新与渲染

        注意到,每次运动都会改变位置,那么就都得对已经渲染过的单元格进行清屏操作,否则就会影响到更新渲染的问题,所以蛇的渲染的总逻辑为:1.清屏 ;2.更新;3.渲染。

4.1 清屏操作

 4.2 蛇的运动更新 

        蛇的运动其实就是蛇在进行 body 数组元素的 增减操作。比如,如果要向右运动,本质就是 body 数组中,对数组元素进行:尾部删除,头部增加 的操作,所以蛇就会渲染新的状态。其实蛇不是真正地动起来了,而是我们对于单元格的渲染而导致蛇的身体发生了变化,而被认为是运动的结果。

基于以上,我们对运动模块进行补全: 

 4.3 蛇的渲染  

        接下来就是蛇的渲染了,渲染逻辑为:Snake 类调用Game类中的 setColor 方法,因为对蛇的渲染实质上就是对 table 的 单元格 的渲染,table 表格是在 Game 类中创建的,所以 渲染颜色的方法 也应该在 Game 中设置: 

 4.4 蛇的相关方法的调用(清屏--更新--渲染)

        在这里,之所以要用一个定时器来获取实例 game 并调用其方法,是因为如果在 Game() 中直接调用,由于 new Snake 也是 Game 的一部分,snake 实例创建的过程还未完成,这时候就直接调用 实例的方法 就会返回一个 undefined 值,而导致获取不到 game 实例。所以这里要用一个定时器来调用其方法,不影响到 Game 的部分,因为定时器是异步调用。

 5.键盘监听事件

        由于用户在玩游戏时,是通过按下键盘来完成控制蛇的运动方向的操作的,所以需要增加一个键盘监听事件。同时,为了方便操作,我们把上述的定时器事件作为 Game 的原型上的一个方法,且增加一个属性 timer。如下: 

5.1 将定时器事件绑定在Game原型上 

        补充:如果想改变贪吃蛇运动的速度,只要修改定时器的触发时间就可以了,时间越短则运动速度越快,反之。

 

 5.2 键盘监听事件方法的书写

 5.3 运动的合理性规划——解决原地调头的问题

       保证蛇运动的合理性: 蛇的运动不能直接掉头。如正在向右运动的蛇,如果想要向左转,就必须要先向下或者上运动一段距离,才能实现左转,否则就是不合理的。

 所以,在进行按键的时候,都必须得先进行判断。即按下的按键所对应的控制方向不能与当前的运动方向相反。

        第一次优化的代码如下:

         不足:当我们用较快地速度按下不同的按键时,仍然会出现原地调头的情况。这是因为定时器每隔固定时间就会执行一次定时器任务,如果此时以较快地速度进行按键操作,那么就会在间隔时间内有了触发,但此时由于时间问题,渲染是在我们设置的500ms之后发生的,就会出现还未完成渲染,但是运动却发生了变化的情况。如,蛇的当前运动方向为 向右运动,若此时按下‘S’键,紧接着按下'A'键,由于两次按键速度过快,定时器来不及执行第一次的‘S’键的渲染(即方向改变的过程)就执行了第二次'A’键的渲染,而发生了原地调头的情况。由上,我们还得需要对按键的响应事件再次进行优化。由于是因为在一次渲染之前出现的调头情况,我们可以通过对将要改变的方向作为突破口来寻找解决方案。

        解决方案:由于我们一开始是 按键按下之后 就直接修改 键值所对应的方向,这样就会导致每按一次键都会对方向的变化产生影响,那么在较快的时间内按下多个键,就有可能出现原地调头的情况,所以我们要用一个 中介值 来对 方向改变的量进行赋值。如下,是利用一个 willDrection 来传递 将要改变的方向 的值。如,当蛇正在向右运动,此时按下'S'键(向下运动'D'),这时不再直接改变方向,而是先调用 changeDirection(‘D’),并将 向下运动的值 作为参数传入到这个函数中,赋值给 willDirection。然后在蛇的 update 方法中,willDirection 会把自己的 方向改变值 赋值给 当前的方向(this.direction),然后根据当前被赋值过的 direction 的值,进行方向的改变。这样的话,即使在定时器时间(设为500ms)内再按下‘A’键(向左运动'L'),由于  update 是要每 500ms 执行一次的,所以即使现在改变了 willDirection 的值,但是由于还没有到 update 的下一次执行时间, willDirection 也就没有机会将它的值 赋值 给 direction, 这样就不会有任何影响了。

具体优化部分如下:

 6.判定游戏中蛇死亡——游戏结束的方式

        蛇死亡,即游戏结束的方式有两种:一种是蛇头撞墙,另一种是蛇头撞到自己的身体。在判定游戏结束时,记得一定要删除尾元素,因为当前蛇头所处位置是临界位置,是非法的,所以在此处时是不能进行正常的在头元素处增加1的。所以为了看起来像是没有增加1,就把尾元素删除了。

        由于与运动,即更新有关,则将判断游戏结束的方法放在 update 中,每次位置的更新都会判断游戏是否结束。

 7.关于蛇的食物的创建 

       7.1 创建一个 Food类,用来产生食物

        7.2 随机产生食物的初始化设置——食物不能出现在蛇的身上

        在食物随机产生时,有可能会出现在蛇的身上,即食物的渲染到了蛇的身上,这是不符合游戏规则的,所以在进行随机产生食物的时候要先对产生的食物的位置进行判断,是否是在蛇的身上?如果在则重新产生一个随机食物;如果不在,则随机产生食物成功。

具体代码及解释如下:

 8 蛇吃食物 & 蛇运动速度的更新

        蛇在游戏过程中,会通过吃食物的方式来增长蛇身的长度,并且在蛇身变长的同时,蛇的运动速度也会加快。由于这两个变化都是在蛇运动过程中产生的,所以我们将这两个模块放在蛇运动的模块中,即 update 中。如下:

8.1 蛇吃食物——蛇身变长

 8.2 对 被吃掉的食物 的清除操作

        蛇吃到食物就得对当前食物进行 清除 操作,这部分在 clear 部分中进行

8.3 蛇运动速度的加快 

 8.4 分数的增加

 9.完整代码

9.1 Game.js部分

function Game(){
 //  alert('欢迎进入贪吃蛇游戏')

   //设置行数和列数
   this.row=20
   this.col=20

   //初始化节点
   this.Init()

   //实例化 蛇类   将蛇类的实例化绑定在了 Game 身上
   this.snake=new Snake()

   //执行定时器任务
   this.start()

   //键盘的事件监听
   this.bindEvent()

   //实例化 食物类,创建一个食物实例
   //初始化 食物  由于需要对食物进行判定,是否被吃掉了,所以需要传入当前对象 this
   //这里的 this 表示 game
   this.food=new Food(this)

   // 分数
   this.score=0
}


//1.在对象的原型上进行 初始化方法 的书写   页面的初始化
Game.prototype.Init=function(){

   //1.1 创建一个 table 标签元素  表单
   this.dom=document.createElement("table")

   var tr,td
   //1.2 遍历行和列
   for(var i=0;i<this.row;i++){
      //1.2.1 遍历行,创建节点上树    行
      tr=document.createElement("tr")

      for(var j=0;j<this.col;j++){
      //1.2.2 遍历列,创建节点上树    单元格   追加到 tr 中,作为 tr 的子元素
         td=document.createElement("td")

      //1.2.3 追加节点上树   使每一行都有 20 个单元格
         tr.appendChild(td)        
      }

      //1.2.4 插入节点tr,其 父节点为 this.dom,即 table 中的一个子元素
      this.dom.appendChild(tr)
   }

   //1.3 表格上树,即插入 table 至 app中
   //获取 id 为 app 的元素,并使 table 上树,即 div 中加入一个子元素 table
   //此时可以发现 table 有 20 个 tr,即 20行
   document.getElementById("app").appendChild(this.dom)
}


//2.清屏操作   每一次更新 渲染之前都应该要进行一次 清屏操作。避免上一次渲染的影响
Game.prototype.clear=function(){
   //遍历表格,清除画布
   for(var i=0;i<this.row;i++)
   {
      for(var j=0;j<this.col;j++)
      {
         //将蛇身设置为与画布一样的颜色,默认为白色
         this.dom.getElementsByTagName("tr")[i].getElementsByTagName("td")[j].style.background='lightgreen'

         // 对食物的清除操作,即将 传入的值 等于 ' '  空字符串
         this.dom.getElementsByTagName("tr")[i].getElementsByTagName("td")[j].innerHTML=' '
      }
   }
}


//3.设置颜色的方法   渲染
Game.prototype.setColor=function(row,col,color){
   //获取到表格的 某行元素 再获取到 该行的某个单元格 ,即可开始进行颜色设置
   //即让表格的 第几行第几列设置成某个颜色
   // getElementsByTagName("tr")  通过 标签名 获取元素
   this.dom.getElementsByTagName("tr")[row].getElementsByTagName("td")[col].style.background = color
}

//5.渲染食物方法
Game.prototype.setHTML=function(row,col,html){
   // 操作元素 超文本 内容     会在 检查工具中的 elements 看到 html的内容发生改变
   // 1.获取:元素.innnerHTML
   // 2.设置:元素.innerHTML='新内容'
   // 这里的 innerHTML 的内容 就是 传入 html的内容
   this.dom.getElementsByTagName("tr")[row].getElementsByTagName("td")[col].innerHTML= html
}

//4.设置键盘监听事件
Game.prototype.bindEvent=function(){
   //4.1记录 dom  这里的 this 是 dom
   var self=this

   //4.2 键盘事件
   document.onkeydown=function(event){
      //获取按下的 按键 的对应的按键号 
      //  console.log(event.keyCode,'event')
       // a/A(右键)---65   s/S(下键)---83    d/D(左键)---68   w/W(上键)----87

      //注意:在这里的 this 是键盘事件函数 的 this,而不是 dom 了,所以要对 dom 进行获取
      switch(event.keyCode)
      {
         case 65:   //按下 左键
         //4.2.1 先进行判断,若当前方向是向右运动,则不能按左键.  以下同理
            if(self.snake.direction == 'R')  return;
            self.snake.changeDirection("L");
            break;

         case 83:   //按下 下键
         if(self.snake.direction == 'U')  return;
            self.snake.changeDirection("D");
            break;

         case 68:   //按下 右键
         if(self.snake.direction == 'L')  return;
            self.snake.changeDirection("R");
            break;

         case 87:   //按下 上键
         if(self.snake.direction == 'D')  return;
            self.snake.changeDirection("U");
            break;
      }
   }
}

//4.有关蛇的方法的调用
//之所以要用一个定时器来获取实例 game并调用其方法,是因为如果在 Game() 中直接调用,由于 new Snake 也是 Game 的一部分,
//snake 实例创建还未完成,就直接调用 实例的方法 就会返回一个 undefined 值
//所以这里要用一个定时器来调用其方法,不影响到 Game 的部分,即异步调用
Game.prototype.start=function(){

   //4.5 速度的更新——加快
   //4.5.1  加入 帧编号,用来描述 蛇的运动速度   初始化为0
   this.frame=0

 this.timer=setInterval(function(){
   // 定时器里面的本质就是 游戏的渲染本质:1.清屏  2.更新  3.渲染 

   // 4.5.2 使 frame 在定时器中 递增 
   // 这里表示每 20ms 增加一次 .
   game.frame++
   document.getElementById("frame").innerHTML="帧编号:"+game.frame;

   // 4.6 渲染分数
   document.getElementById("score").innerHTML="分数:"+game.score;

   //4.1 清屏操作
   game.clear()

   // 4.5.3 蛇的速度更新,蛇的长度变长时,速度要加快
   var during=game.snake.body.length<30 ? 30-game.snake.body.length : 1
    //4.2 蛇的运动  即蛇的更新
   game.frame%during==0 && game.snake.update()

   //4.3 蛇的渲染    每20ms 渲染一次
   game.snake.render()

   //4.4 食物的渲染
   game.food.render()
 
},20)
}

 9.2 Snake.js 部分

function Snake(){
    //蛇的初始化身体
    this.body=[
         //利用表格的 几行几列 来表示蛇的位置 行与列都是从 0 开始
         //这里表示蛇的初始位置为 第一行的1,2,3,4列,即蛇的长度为 4 个单元格长度
         //以第0项为蛇头,所以将 col 的顺序倒换一下,即蛇头在右边,蛇身在左边
         {"row":0,"col":4},
        {"row":0,"col":3},
        {"row":0,"col":2},
        {"row":0,"col":1},
        {"row":0,"col":0}
    ]

    //设置一个运动方向的信号量,初始化为 "R"
    this.direction="D"

     //即将改变的方向,目的就是为了防止蛇的运动方向出现原地调头的情况
   //设置初始改变方向为 向右运动
   this.willDirection="R"
}

//2.蛇的运动        每一次运动就是更新的效果  
Snake.prototype.update=function(){
    // 蛇的身体是一个 数组类型,蛇的长度的增减,即数组元素的 增加 与 删除
    //这里就要考虑到:由于对于 蛇的渲染 是对单元格的渲染,无论是 蛇的运动,还是蛇长度的增减,
    //对于已经 渲染过的且不需要再保持渲染效果的蛇的部位(单元格),就得进行清除操作
    //那么总结起来就是得进行 : 清屏,更新,渲染  
    //  每一次运动就是更新的效果  ,所以 蛇的运动 应该要发生在 蛇的渲染 之前

    //2.1 让当前的direction,接收一下willDirection
    this.direction=this.willDirection

    switch(this.direction)
    {
        case "R":   //向右运动
            this.body.unshift({"row":this.body[0].row,"col":this.body[0].col+1})
            break;
        case "D":   //向下运动
            this.body.unshift({"row":this.body[0].row+1,"col":this.body[0].col})
            break;
        case "L":   //向左运动
            this.body.unshift({"row":this.body[0].row,"col":this.body[0].col-1})
            break;
        case "U":   //向上运动
            this.body.unshift({"row":this.body[0].row-1,"col":this.body[0].col})
            break;
    }
    

    //4.对蛇的死亡判断,游戏结束

    //4.1 蛇撞墙死亡,临界判断
    //一碰墙就判定 游戏结束     所以 右边和下边的临界值设为 game.col-1  gama.row-1; 左边和上边的临界值为  0
    if(this.body[0].col>game.col-1 || this.body[0].row>game.row-1 || this.body[0].row<0 || this.body[0].col<0)
    {
        alert('游戏结束')

        //删除尾元素
        this.body.shift()
        
        //clearInterval() 方法可取消由 setInterval() 函数设定的定时执行操作。
        // clearInterval() 方法的参数必须是由 setInterval() 返回的 ID 值。
        clearInterval(game.timer)
    }

    //4.2 蛇头撞到自己的身体,判定结束
    //蛇头撞到自己身体,则遍历时要从 身体 开始遍历,即 col为1 开始遍历
    for(var i=1;i<this.body.length;i++)
    {  
         //蛇头与身体某个部位的位置重合时,即表示蛇头撞到自己了
        if(this.body[0].col == this.body[i].col && this.body[0].row == this.body[i].row)
        {
            alert('游戏结束')
            //删除尾元素
            this.body.shift()
            clearInterval(game.timer)
        }
    }


    //5. 蛇身与蛇速的更新

    //5.1 蛇身变长
    // 蛇吃到食物,即蛇头位置与食物位置重合
    if(this.body[0].row == game.food.row && this.body[0].col == game.food.col)
    {
        // 吃到食物了
        // alert("吃到食物了")

        // 增加一个 长度 到 蛇body 的 末尾元素 
        // 这里如果直接这样写,就会出现 蛇身 闪动的样子。这是因为蛇在运动的时候都会进行 头部加1,尾部减1 操作,就会有闪动的效果。
        // 要解决这个问题,要清楚:尾部不删,头部加1,就是代表长度增加。
        // 所以要增加一个 尾部删除 操作的允许操作条件,即没有吃食物的才进行 尾部删除,头部加1;吃到食物时,只进行 头部加1
        // this.body.push({'row':this.body[lenght].row, 'col':this.body[lengtg].col})
 
        //5.2 吃到食物  只有 switch-case 中的 头部加1,不进行 尾部删除,长度加1
        //5.2.1 创建新的食物
        game.food=new Food(game)

        //5.2.2 防止蛇吃到食物后更新速度时会 蹿一下,时 帧 归零,从头计算。每20ms 渲染一次
        game.frame=0

        // 5.2.3 分数增加
        game.score++;
    }
    else
    {
        // 5.3 没有吃到食物  进行正常长度的运动,头部加1 尾部删除

        //蛇的不同方向的运动,都要对 尾部元素 进行 删除操作
       this.body.pop()

    }

    // 5.4 蛇吃到食物就得对当前食物进行 清除 操作,这部分在 clear 部分中进行
    
}

//3.蛇的改变方向,防止的是在一次渲染之前会出现调头的情况
Snake.prototype.changeDirection=function(dir){
    this.willDirection=dir
}

//1.render()      渲染模板
Snake.prototype.render=function(){
    // console.log(game)    用来测试是否成功 获取相关元素以及是否能成功使用

    //蛇的渲染  ---- 在表格上进行渲染
    // body 是数组形式,body[n],表示第n个元素
    //设置好蛇头颜色,蛇头颜色与蛇身设置成不同的颜色  蛇头设置为粉色
   game.setColor(this.body[0].row,this.body[0].col,'pink')

   //开始遍历 蛇身,并设置成相同的颜色  黄色
   for(var i=1;i<this.body.length;i++)
   {
       game.setColor(this.body[i].row,this.body[i].col,'yellow')
   }
}

9.3 Food.js 部分

//1. 创建食物类
function Food(gameSnake){
    //alert('吃东西喽')

    //备份 当前 this
    var self=this
    //1.1 食物的位置  随机
    //由于食物可能会随机出现在蛇的身上,即会渲染到蛇的身上。所以在随机产生食物的时候,
    //要先判断食物的 row 和 col 是否在蛇的身上
    // 这里用 do-while 循环语句来进行判断处理,作用是先创建一个 row 和 col ,然后再进行判断row和col是否在蛇的身上
    //while 中写一个判断方法,作为循环条件
    do{
        this.row=parseInt(Math.random() * gameSnake.row);
        this.col=parseInt(Math.random() * gameSnake.col);
    } while( (function(){
        //遍历蛇的 row 和 col,然后与随机产生食物的 row 和 col 进行比较,判断是否重合
        for(var i=0;i>gameSnake.snake.body.length;i++)
        {
            //注意:在这个方法函数里,this 不再是指 食物类的对象 了,而是 window 了。
            // 所以要 引用 食物对象 就得在上面进行 备份
            if(gameSnake.snake.body[i].row == self.row && gameSnake.snake.body[i].col == self.col)
            {
                //如果 食物 在 蛇的身上,那么就返回 true 值,继续循环,产生 新的随机食物
                return true;
            }
        }
          // 如果食物不在蛇身上,说明该 随机食物 是合法的,就不用再重新生成一个随机食物了,返回 false 值
          return false;
    })())
    //  console.log(this.row,this.col)           测试每次产生食物的位置是否是不一样的
}

//食物渲染
Food.prototype.render=function(){
    game.setHTML(this.row,this.col,"❤")
}

9.4 html 部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>贪吃蛇游戏</title>

    <style>
        body{
            background-color: aqua;
        }
        table{
            /* border-collapse属性,属性值collapse可以使边框合并,默认值sparate边框分开 */
            border-collapse: collapse;
            border: 1px solid #333;
            background-color: lightgreen;
        }

        td{
            width: 20px;
            height: 20px;
            border: 1px solid #aaa;
            /* 设置文本颜色    食物颜色  */
            color:red;
            text-align: center; 
        }
        img{
            width: 100px;
            height:150px;
        }

        audio{
            margin-left: 600px;
            background-color: blue;
        }
    </style>
  
</head>
<body>
    <img src="../图片资源/我的照片.jpg" alt="">
    <h2 id="frame">帧编号:0</h2>
    <h2 id="score">分数:0</h2>
    <div id="app"></div>
    <audio src="../音频格式转换器/王心凌 - 爱你.mp3" controls loop></audio>

    <!--1. 引入js文件   Game类 -->
    <script src="1.Game.js"></script>

    <!-- 2. 蛇    Snake类 -->
    <script src="1.Snake.js"></script>

    <!-- 3. 食物    Food类 -->
    <script src="1.Food.js"></script>

    <script>
        var game=new Game()
    </script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值