Phaser3初体验

Phaser3初体验

译者说:

1、本文讲述的是作者初次使用Phaser3(之前都用Phaser2)制作游戏的一些心得和碰到的坑。

2、为了跟代码对应,有些单词不译成中文(比如scene、image)。

3、初次翻译教程,加上水平所限,疏漏之处欢迎指教。

4、原文分为三部分,链接在此:原文1原文2原文3

5、译者:大吃货,转载请注明。

6、Phaser2还没学会就出了Phaser3…不带这么坑爹的。

===========================================================================

Part1:

项目介绍这个项目故意做得比较小,所以我不会在学习过程中讲得过于详细。我将它设定为一个休闲益智游戏,可通过触屏输入(或单击)。这是一个很多人从小就知道的纸牌游戏----翻牌记忆大作战。游戏的名字就叫Pixel Memory吧。

概述:挑战Phaser3

本文的重点是向大家展示我使用Phaser3的过程,并分享我的一部分代码。在我使用Phaser3的第一天,我遇到了以下的挑战,我将在下文一一阐述。

1、Phaser 3 游戏设置

2、设置全局变量

3、添加scene

4、游戏的闭包(原文enclosing,个人觉得“闭包”比“打包”更适合)

5、包含场景和预设的独立文件

6、Canvas的居中和尺寸调整

7、全屏化

8、设置Bitmap Text 的原点

1、Phaser 3 游戏设置

第一个挑战是:我该怎样用一种首选的配置来启动游戏?

在Phaser3中用一个对象作为游戏的构造器,这个对象包含了游戏的各种配置属性。

这些属性是很重要的,从官网的例子就可以看出来。

var config = {
          // ...
};
var game  = new Phaser.Game(config);

你会发现很多的新属性,它们非常酷!其中之一就是游戏的name。parent属性就是包含了游戏canvas的div的ID。

下面就是我的游戏设置对象

var config  = {
          type      : Phaser.AUTO,
          width     : 9  * 64,           // 576
          height    : 15 * 64,                     // 960
          parent    : 'phaser-app',
          scene     : scenes,
          title     : 'PixelMemory'
};

var game  = new Phaser.Game(config);

2、设置全局变量

在Phaser2中我常做的一件事就是设置一个可以从每个scene调用的全局变量(在Phaser3中state变成scene)。如果我需要从许多场景中访问一个属性,我选择设置全局变量,而不是在场景之间传递它们。此外,在场景之间传值目前还存在bug。见github

下面看看我是怎样设置全局变量的:

game._URL = 'http://localhost/PhaserGames/PixelMemory/'
game._USER_ID = 0;

稍后在任意场景中调用这个变量的方法:

var url  = this.sys.game._URL;
var u_id  = this.sys.game._USER_ID;

3、添加scene

在Phaser3中,我们把scene添加到一个数组中,再把这个数组添加到config对象的scene属性中。虽然我喜欢这种用数组传递的方式,但最开始我还是有点困惑:scene的启动顺序是否跟它在数组中的顺序有关呢?

此外,如果你添加了很多的scene,这种方式的可读性就会变差。为了提高代码的可读性,我通过这种方式添加scene:

var scenes  = [];
 
scenes.push(BootScene);
scenes.push(PreloadScene);
scenes.push(IntroScene);
 
var config = {
          // ...
          scene: scenes,
          // ...
};
 

除了你添加的第一个scene,其他的scene顺序无关紧要。默认情况下,游戏将自动从数组的第一个scene启动。请注意你添加的第一个scene。当然,你也可以手动设定为从任意的scene启动游戏。

4、游戏的闭包

浏览器端游戏跟原生应用不同之处在于,前者的代码很容易在浏览器上暴露出来。我喜欢尽可能的把函数闭包,这样任何人想要在游戏运行时修改游戏就变得不那么容易(破解、修改)。基于以上原因,我一直喜欢把游戏进行闭包。我不知道这能多大程度保护游戏,但聊胜于无。我相信,你做出的这些努力,会让玩家感受到你对你的游戏有多在乎。(我总是想看看自己能否轻易地破解H5游戏,像我这样的人肯定不少)

在Phaser2中,我用这种简单的方式实现游戏闭包:

var App = function() {};
 
App.prototype.start = function()
{
          // Scenes
          var scenes = [];
          
          scenes.push(BootScene);
          scenes.push(PreloadScene);
          scenes.push(IntroScene);
          
          // Game config
          var config  = {
                    type      : Phaser.AUTO,
                    width     : 9  * 64,           // 576
                    height    : 15 * 64,                     // 960
                    parent    : 'phaser-app',
                    scene     : scenes,
                    title     : 'PixelMemory'
          };
          
          // Create game app
          var game  = new Phaser.Game(config);
          
          // Globals
          game._URL = 'http://localhost/PhaserGames/PixelMemory/';     // this.sys.game._URL
          game._USER_ID = 0;
          
          game._CONFIG = config;
};
 
window.onload = function()
{
          'use strict';
          
          var app = new App();
 
          app.start();
}

5、包含场景和预设的独立文件

出于代码组织的原因(也可能是个人喜好),我喜欢为每个scene和预设建立单独的js文件。闭包和原型(prototyping)帮助我保持一个干净的命名空间。此外,如果我的代码整齐地组织在这样的文件中,我会更好地专注于当前的任务。

根据官网的例子,最好在场景中使用静态方法,而不是把它们原型化。之前传给scene对象的name字符串,以后可以在启动这个scene时直接调用(或切换,Phaser3的新功能)。

下面就是我组织scene的方式:

var PreloadScene= new Phaser.Scene('Preload');
PreloadScene.preload = function()
{
          'use strict';
          
          // ...
}; 
PreloadScene.create= function()
{
          'use strict';
          
          // ...
}; 
PreloadScene.update= function()
{
          'use strict';
          
          // ...
};

这个是预设文件:

var Helper = function() {};
Helper.prototype.createText = function(ctx, x, y, string, size, anchor)
{
          'use strict';
};

如何从当前场景开始一个新场景:

this.scene.start('Preload');

6、Canvas的居中和尺寸调整

在Phaser2中我们可以很方便地配置canvas的位置和尺寸。但Phaser3中还没有类似的方法。所以我得用老办法,在index.html中添加一些css和js代码。

对于这个游戏,我希望它始终在屏幕居中,并随着浏览器窗口调整大小,并且可以适配移动端。

下面是我的css代码(‘phaser-app’是我在config对象中配置的parent属性)

body {
          margin: 0;
          overflow: hidden;
          background-color: black;
}
                    
canvas {
          height: 100%;
}
                    
#phaser-app {
          margin: 0 auto;
}

js代码:

// Resize
function resizeApp()
{
          var div = document.getElementById('phaser-app');                    
          div.style.width = window.innerHeight * 0.6;
          div.style.height = window.innerHeight;
}                   
window.addEventListener('resize', function(e)
{
          resizeApp();
});                   
resizeApp();

7、全屏化

在Phaser2中我们可以方便地使游戏全屏启动。但在Phaser3Beta20中似乎没有这样的功能(至少原文作者没找到这个功能),所以我必须手写全屏代码。

此代码在我的index.html文件中:

// Fullscreen
function fs_status()
{
          if(document.fullscreenElement)
          {
                    return true;
          }
          else if(document.webkitFullscreenElement)
          {
                    return true;
          }
          else if(document.mozFullScreenElement)
          {
                    return true;
          }
          else
          {
                    return false;
          }
}                    
function goFullscreen()
{
          if(fs_status())
          {
                    return;
          }
                               
          var el = document.getElementsByTagName('canvas')[0];
          var requestFullScreen = el.requestFullscreen || el.msRequestFullscreen || el.mozRequestFullScreen || el.webkitRequestFullscreen;
                               
          if(requestFullScreen)
          {
                    requestFullScreen.call(el);
          }
}
document.getElementsByTagName('div')[0].addEventListener('click', goFullscreen);

8、设置Bitmap Text 的原点

目前为止,我们还无法改变bitmap text的原点,但是可以改变image的原点(没试过其他对象)。

Phaser3舍弃了‘anchor’,取而代之的是‘origin’。

Phaser3默认情况下,image的原点就设置在了0.5(Phaser2是0),但是bitmap text的原点默认还是0。

设置一个image的原点:

// You can chain it directly
this.add.image(0, 0, 'bg-main').setOrigin(0);
 
// Or you can add it later to an image object
this.bg_main.setOrigin(0);

为了把bitmap text的原点设置在中心,我写了这个辅助方法:

// My helper prefab
var Helper = function() {};
 
// Method to create a bitmap text
// I am still calling it "ancho"r instead of "origin"; old habit that will change going forward
Helper.prototype.createText = function(ctx, x, y, string, size, anchor)
{
          'use strict';
          var text;          
          var font  = 'supermercado';
          var size  = size || 64;
          
          // Text
          text = ctx.add.bitmapText(x, y, font, string, 64);
          
          // Anchor...
          // ...center
          if(!anchor || anchor === 0.5)
          {
                    text.x  -= (text.width * 0.5);
                    text.y  -= (text.height * 0.5);
          }
          // ...1
          if(anchor === 1)
          {
                    text.x  -= text.width;
                    text.y  -= text.height;
          }
          // ...x & y different
          else if(typeof anchor == 'object')
          {
                    if(anchor.x === 0.5)
                    {
                               text.x -= (text.width * 0.5);
                    }
                    if(anchor.y === 0.5)
                    {
                               text.y -= (text.height * 0.5);
                    }
                    
                    if(anchor.x === 1)
                    {
                               text.x  -= text.width;
                    }
                    if(anchor.y === 1)
                    {
                               text.y  -= text.height;
                    }
          }
          
          // Return
          return text;
};

===========================================================================

Part2:

概述:

1、在场景之间传值。

2、改变对象的宽高。

3、配置tween(补间动画)。

4、精灵的子对象:带标签的按钮。

1、在场景之间传值

2018.2.25更新:这个bug在3.1.2中已被修复。详情参考这个帖子,特别是对全局变量的使用。

如上文所说,Phaser3Beta20在场景中传递数据会有bug。正常来说,我们是这样在场景中传值的:

// Here we are in the "Level" scene
// We start the "play" scene and send some data
this.scene.start('Play', { level: 3, difficulty: Medium });
 
// In the init or create method of the "Play" scene you receive the data as follows
PlayScene.init = function(data)
{
          this._LEVEL = data.level;
          this._DIFF  = data.difficulty;
};

不幸的是,在PlayScene场景接收到的值是空的。

在我的小游戏中,玩家可以选择难度,还可以指定卡片的颜色。由于我无法把这个值传给PlayScene,我只好使用全局变量来解决这个问题。

在DecksScene场景选择卡片颜色:

DecksScene.clickBlue = function()
{
          'use strict';
          
          if(this.flagClick() === false)
          {
                    return;
          }
          
          this.sys.game._DECK = 0;
          
          this.hideUnselected();
          
          this.startTransitionOut(this.goPlay);
};

在DifficultyScene场景选择难度:

DifficultyScene.clickEasy = function()
{
          'use strict';
          
          if(this.flagClick() === false)
          {
                    return;
          }
          
          this.sys.game._COLS = 4;
          this.sys.game._ROWS = 7;
          
          this.startTransitionOut(this.goDecks);
};

初始化PlayScene场景时,给它的配置赋值:

PlayScene.init = function()
{
          'use strict';
 
          // ...
 
          this._COLS  = this.sys.game._COLS;
          this._ROWS  = this.sys.game._ROWS;
          this._DECK   = this.sys.game._DECK;
 
          // ...
 
          this.sys.game._COLS = undefined;
          this.sys.game._ROWS = undefined;
          this.sys.game._DECK = undefined;
};

这个想法很简单,只要你设置的值正确,程序就可以正常运行。

重要提示:如果你想在全局空间保存一个变量,就必须把它保存在this.sys.game里面。

2、改变对象的宽高

Phaser2中你可以直接设置一个游戏对象的宽高,这些数值也会确实地应用在游戏中。

在Phaser3中可就没这么简单了。在Phaser3中,你必须弄清楚sprite.width和sprite.displayWidth的区别。比如,我想动态改变一个玩家配置文件中的经验条宽度,你就必须设定displayWidth属性。

现在width属性被保留为image的宽度。如果一个sprite sheet含有width属性,则它表示的是sprite sheet一帧的宽度值。你也可以通过缩放来改变精灵的宽度。但是请记住,这个操作会缩放整个image!由于我的经验条有长短不同的边,所以我不能缩放整个image。我需要动态地改变宽度,于是操作displayWidth值就可以实现了。

// This does nothing
this.ui.profile.xpbar.width = Math.round(this.ui.profile.xpbar.width * ratio);
 
// This works
this.ui.profile.xpbar.displayWidth  = Math.round(this.ui.profile.xpbar.width * ratio);
 
// This also changes the width, but it scales the full width of the image!!
this.ui.profile.xpbar.setScale(ratio, 1);

3、配置tween

Phaser3的补间动画简直棒极了!

我们只是简单地给一个对象传递了补间动画的配置属性,比如duration、ease,还有callback和callbackScope。

下面这个例子对于了解补间动画属性非常有用,如callbackScope。试试吧!

var scope = this;
 
var tween = this.tweens.add({
        targets: [ myImage, myGraphic, mySprite ],
        x: 600,
        ease: 'Linear',
        duration: 3000,
        yoyo: true,
        repeat: 1, // -1 for infinite repeats
        onStart: function () { console.log('onStart'); console.log(arguments); },
        onComplete: function () { console.log('onComplete'); console.log(arguments); },
        onYoyo: function () { console.log('onYoyo'); console.log(arguments); },
        onRepeat: function () { console.log('onRepeat'); console.log(arguments); },
        callbackScope: scope
    });

有时你可能创建了一些非常相似的补间动画,它们只有很小的差别。在这种情况下,你就可以创建一个方法,来为你返回补间动画的配置对象。

function getTweenConfig(ctx, delay, col, row, pos)
{
          return {
                    targets: ctx.decks[col][row],
                    delay: delay,
                    duration: 500,
                    x: pos.x,
                    y: pos.y,
                    angle: -720,
                    ease: 'Linear',
                    // play sfx
                    onStart: function() { ctx.time.delayedCall(delay, ctx.helper.playSfx, [ctx, 'tap_card'], ctx); },
                    // normal callback
                    onComplete: function() { completeIntro.call(ctx, col, row); },
                    callbackScope: ctx
          }
}

4、精灵的子对象:带标签的按钮

我喜欢为对象创建辅助方法,我会在我的游戏中多次这么做。

这个对象就是带标签的按钮。虽然很多时候按钮是相同的,但它们的标签从来都不一样。

在Phaser2中,你可以创建一个按钮,并将一个文本作为它的子对象。无论你改变按钮的什么属性,都会应用到它的子对象(文本)上。不幸的是,在Phaser3Beta20中,你不可以给一个对象添加子对象。我希望在以后的版本中可以实现这个功能。虽然你依旧可以创建Phaser组,但是组对象也没有提供我的按钮需要的所有功能。所以,我只好将文本添加到按钮的data对象中。

我会在下面贴出方法的完整代码,你可以用在自己的代码中并进一步完善它:

Helper.prototype.createBtnWithLabel = function(ctx, x, y, img, callback, label_config, frames, data)
{
          'use strict';
          
          var btn;
          var text;
          
          var label_config = label_config || { string: '[n/a]', size: 64, color: '0xFFFFFF', x: 0, y: 0 };
          
          // Label position
          if(!label_config.x)
          {
                    label_config.x  = 0;
          }
          if(!label_config.y)
          {
                    label_config.y  = 0;
          }
          
          // Create...
          // ...sprite
          btn  = ctx.add.sprite(x, y, img);
          // ...label
          text  = this.createText(ctx, x + label_config.x, y + label_config.y, label_config.string, label_config.size, label_config.color);
          // ...data
          btn.data = data || {};
          btn.data.label_obj  = text;
          
          // Inputs...
          // ...activate
          btn.setInteractive();
          // ...callback
          btn.on('pointerup', function(e)
          {
                    ctx.helper.playClickSfx(ctx);
                    callback.call(ctx);
          });
          
          // Frames...
          // ...hover
          if(frames && frames.over)
          {
                    btn.on('pointerover', function(e)
                    {
                               this.setFrame(frames.over);
                    });
                    
                    btn.on('pointerout', function(e)
                    {
                               this.setFrame(0);
                    });
          }
          // ...click
          if(frames && frames.down)
          {
                    btn.on('pointerdown', function(e)
                    {
                               this.setFrame(frames.down);
                    });
          }
          
          // Return group
          return btn;
};
    如你所见,这种方法的一个妙处在于,你可以轻松地为所有的按钮添加或者改变点击音效。你还可以为移动设备的播放器添加回调函数,使得你的一个触屏动作不会触发退出状态。举个例子,执行注册的pointerdown输入时,你可以运行一个回调方法,稍后(比如250毫秒)将按钮重置到“out”帧。希望这个方法可以为你的按钮提供很好的参考。

===========================================================================Part3

今天官方放出了Phaser3,尽管只是一个beta版本。请继续关注我用Phaser3Beta20制作我的第一个Phaser3游戏。

虽然我现在提及的一些东西以后可能会被修复或者改变,但我会尽量为你们讲解一些以后不太可能改变的知识点。

概述:

1、没有补间的Preload

2、Graphics对象的特性

3、重新开始同一场景4、播放音频

1、没有补间的Preload

这个第一段讲述的是我的发现,而不是教程。

我想为我的游戏创建一个自定义的加载动画,而不是使用从左到右加载的标准进度条。我想在游戏加载时慢慢地把卡片翻转过来。

补间一开始运行正常,可是在加载资源时出错了。这是因为补间动画只能在场景的preload方法执行完毕才能运行。

问题是,所有的资源都是在preload方法中加载的,而preload方法在create方法之前执行。所以当执行create方法时,再运行加载动画就没有意义了。即使我在preload方法中创建补间动画,也得等到场景执行create方法时才能触发动画。手动迫使补间动画在preload方法中执行也失败了。

所以我就在想,在执行preload方法期间,进度条的宽度是如何变化的呢?因为它不是一个补间动画!实际上,当图像、音频、字体这些文件加载时,每次加载后的回调函数都会触发进度条图像的宽度改变。

this.load.on('progress', function(value)
{
          txt_percent.text = Math.round(value * 100) + ' %';
});

这意味着,只要能调用进度回调函数,就能创建自定义加载动画。

Phaser的加载进度表示为0到1之间的浮点数,再把它乘以100,就可以表示当前的加载进度。因此,如果你能想到一个不同的动画,并且把它跟数值联系起来,就能做出自定义加载动画。就像进度条图像一样,只需要数值参数就可以改变它的displayWidth属性。

但是补间动画只能在所有资源加载完成后才能运行。

2、Graphic对象的特性

我对Phaser2的Graphic对象太熟悉了,以至于我发现Phaser3的Graphic对象是如此惊人的不同。

2.1 X、Y坐标

让我们从创建一个graphic对象开始。我的大多数graphic对象都是UI背景或类似的东西。

我习惯在x,y位置创建graphic,而不是像许多例子一样在0, 0。

我的理由是:如果你在0,0创建了一个graphic对象,并在稍后把它拖拽到x,y,但是对象的实际位置依然在0, 0。如果你想在graphic对象中添加一个文本,并且这个文本的位置是参考graphic对象的,那么这个文本的坐标设为0,0是无效的。

对于整个canvas的原点来说也是一样。当你在Phaser3中创建一个graphic对象时,就再也不能给它传递x和y作为单独的参数了。你必须传递给对象一个x和y的属性。如果你像这样创建graphic对象,那么x和y属性将与对象的位置一致:

// We want our rectangle to be at 25, 100.
var x  = 25;
var y  = 100;
var bg = this.add.graphics({ x: x, y: y });
 
// We make it solid black
bg.beginFill(0x000000, 1);
 
// When you draw the rectangle, you have to start drawing at 0,0 now because
// this position is relative to what you have set in the create method above
var width  = 350;
var height = 200;
bg.drawRect(0, 0, width, height);
 
// We are done!
bg.endFill();

2.2 Graphic的宽度和高度

现在你可以在graphic对象的相对坐标系创建对象了。

不幸的是,we cannot base anything off the graphic’s width and height properties anymore.(这句话实在是没看懂,-_-|)因为某些原因,Phaser3Beta20的graphic对象没有宽度和高度值。因此,如果你需要相对定位,你必须自己设置。

但是我会小心地设置属性名称,以防止跟以后发布的新版本Phaser属性名冲突。我不会使用bg.width,像bg.my_width这样类似的名称就很安全,不会跟未来可能会有的属性发生冲突。

// I wouldn't set these properties
bg.width = 350;

// This is probably never going to be needed by the Phaser 3 framework
bg.my_width = 350;

2.3 Graphic的补间和alpha

最后一个问题是,对于一个已经存在的graohic对象,你无法为它创建补间,或者改变它的alpha。我不知这是否是一个bug,又或者是在Beta20版本中遗漏了。但是如果它很快加入了,那就太棒了。

如果你想改变graphic对象的alpha,你只能先把它清空–重置fillStyle–重绘。

// We create the graphics object
var bg = this.add.graphics({ x: 25, y: 100 });
bg.fillStyle(0x000000, 1);
bg.drawRect(0, 0, 350, 200);
bg.endFill();

// Neither of these will work
bg.setAlpha(0.5);
bg.alpha = 0.5;

// You have to clear & re-draw
bg.clear();
bg.fillStyle(0x000000, 0.5);
bg.drawRect(0, 0, 350, 200);
bg.endFill();

这使得它不适合补间动画。也许一个可能的解决办法就是做一个包含数次循环的补间,然后每次使用onRepeat回调函数来手动清除和重绘。

3、重新开始同一场景

在Phaser2中你可以从一个场景内部启动这个场景本身,也就是场景调用自身的init方法重启。这听起来可能有些乱,但实际上就是这样:重启场景scene(从Phaser2的state变成Phaser3的scene)。

Phaser3中你是无法在当前场景进行‘重启’的。

// If you are currently in the PlayScene, this will not work
this.scene.start('Play');

所以我想出一个办法:创建一个像这样的重定向场景:

var RedirectScene = new Phaser.Scene('Redirect');

RedirectScene.init = function()
{
	'use strict';
	
	// ...
};

RedirectScene.create = function()
{
	'use strict';
	
	this.bg	= this.add.image(0, 0, 'bg-main').setOrigin(0);
	
	this.scene.start('Play');
};

如果场景之间可以传值我们就可以很容易地传递场景的name属性,并让RedirectScene变得更加灵活。如果你想在RedirectScene场景中重启不同的场景,首先你必须设置一个全局变量。

幸运的是,在我的游戏中,我只需要重启PlayScene场景。因此,我只要在RedirectScene中start(‘Play’)就行了。

4、播放音频

在教程临近尾声之时,我想说,在Phaser3中播放音频的功能也很棒。一旦音频文件加载到游戏中,你就可以随时播放它们。不用像在Phaser2中那样先创建一个sound对象了。这虽然是一个小变化,却使得添加音频变得更有乐趣。

// PreloadScene
this.load.audio('level_won', [ 'levelwin.ogg', 'levelwin.m4a' ]);

// PlayScene
this.sound.play('level_won');
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很抱歉,根据提供的引用内容,我无法找到与"phaser3 rgp"相关的信息。可能是没有提供与此相关的引用内容。请提供更多的信息或具体问题,以便我可以更好地回答您的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [phaser3-vjoy-plugin:Phaser3的虚拟操纵杆插件](https://download.csdn.net/download/weixin_42153691/15000739)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [phaser3-flappy-bird:phaser3的软盘鸟示例](https://download.csdn.net/download/weixin_42125826/15082687)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [phaser3-planck:在Phaser3中实现planck.js物理](https://download.csdn.net/download/weixin_42129412/18786991)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值