用html5实现2D效果,Cocos2d-html5《王者之剑》实现(1)(2)(3)

转自泰然网,作者一叶

项目组织

360 度 可触摸摇杆实现

攻击 与 特效攻击

游戏中的操作控制

操作控制之键盘映射

用户信息更新

动作,帧动画 VS 骨骼动画

版本库升级

角色动作组织

05ebf48572fb2488fa3bb0802a86df61.png

《Cocos2d-html5 王者之剑实现(2)》

在 《Cocos2d-html5 王者之剑实现 (1)》 中,实现了触摸摇杆和按钮点击的实现,这里继续。

游戏中的操作控制

使用 H5 来开发 Cocos2d-x 游戏的一个优势是它可以在浏览器中运行,这意味着,你无需安装部署到客户端即可发布运行,这时我们可以将一些游戏当中的触摸操作转换成键盘映射,就像我们平时玩 PC 游戏一样,比如跑动,跳跃等操作。

在之前,我们已经实现了一个摇杆和攻击按钮的实现,但是并没有一个统一的控制管理,以让它们很好的协同工作,这也是我们现在所要做的事情,首先我们有一个游戏层,里面包含了一个控制操作实现:

varGameLayer = cc.Layer.extend({

init:function(){

varbRef =false;

if(this._super()){

// ...

// 添加控制层

varhudLayer = HudLayer.create();

this.addChild(hudLayer);

hudLayer.setDelegate(this);

// ...

bRef = true;

}

returnbRef;

},

actionJoypadStart:function(degrees){

},

actionJoypadUpdate:function(degrees){

},

actionJoypadEnded:function(degrees){

},

attackButtonClick:function(button){

}

});

我们在 GameLayer 里面定义了 四个操作控制方法 ,摇杆控制,和按钮点击,我们实现的这四个方法,将会由 HudLayer 层来进行控制,显然对于 HudLayer 来说,它需要知道什么时候去调用这些方法,而对于 GamerLayer 本身来说,它 只关心 当触发了这些控制操作之时,需要多哪些操作,如触发摇杆操作,我们移动一个人物的走动,点击攻击,触发人物的攻击等。至于什么时候被触发,怎么被触发的,那它完全不关心。那么谁关心呢?显然不是你我 ~~

HudLayer 完成了这样一个功能,它包含了三个攻击按钮和一个摇杆控件,而且将由它 直接或者间接 完成对 四个操作控制方法 的调用。为什么说是直接或者间接?实际上这就是 委托机制 ,四个操作控制方法 就是回调函数,在 GameLayer 将回调函数委托给了 HudLayer ,当然 HudLayer 也可以继续委托给别人做,或者由自己来做。

还记得前文中 ActionButton 实现的 click 方法么!如果忘了,可以回头看看,有一句关键代码this._delegate.attackButtonClick(this.getAttackType());,它便是调用委托回调函数,而 _delegate 是谁,这不重要,重要的是它一定实现了 attackButtonClick 方法。至于什么时候 click 被触发,我想这里可以从前文代码看出来,在onTouchBegan 达成点击触摸操作时,当然包括了一些其它判断,如点击区域检测。这里的 _delegate 是谁:

varHudLayer = cc.Layer.extend({

_winSize: null,

_pCenter: null,

_delegate: null,

mJoypad: null,

mAttack: null,

mAttackA: null,

mAttackB: null,

ctor:function(){

this._super();

_winSize = cc.Director.getInstance().getWinSize();

_pCenter = cc.p(_winSize.width / 2, _winSize.height / 2);

},

init:function(){

cc.log("Hud layer init ..");

varbRet =false;

if(this._super()){

// 添加控制器

this.mJoypad = Joypad.create();

this.addChild(this.mJoypad);

// 添加攻击按钮

this.mAttack = AttackButton.initWithImage(s_Attack);

this.mAttack.setPosition(cc.p(mWinSize.width - 80,  80));

// 设置攻击按钮的 delegate

this.mAttack.setDelegate(this);

this.mAttack.setAttackType(AT.ATTACK);

this.addChild(this.mAttack);

// 其它攻击 ...

// ...

bRet = true;

}

returnbRet;

},

setDelegate: function(delegate){

this._delegate = delegate;

this.mJoypad.setDelegate(delegate);

},

attackButtonClick:function(button){

if(this._delegate){

this._delegate.attackButtonClick(button);

}

},

keyAttack:function(btnType){

if(btnType == AT.ATTACK_A &&this.mAttackA.isCanClick())

this.mAttackA.click();

if(btnType == AT.ATTACK_B &&this.mAttackB.isCanClick())

this.mAttackB.click();

if(btnType == AT.ATTACK &&this.mAttack.isCanClick())

this.mAttack.click();

// this.attackButtonClick(bunType);

},

keyAttackUp:function(btnType){

if(btnType == AT.ATTACK)//  && this.mAttack.isCanClick())

this.mAttack.clickUp();

}

});

攻击按钮的 _delegage 就是 HudLayer 本身,所以在攻击按钮的 click 触发之时,HudLayer 的 attackButtonClick 方法被触发,而 HudLayer 本身的 _delegate 是谁呢,当然是 GameLayer 了,所以最终 GaleLayer 的 attackButtonClick 方法被触发,以达到攻击按钮点击作用于游戏控制的目的。这便是由 HudLayer 直接控制 GameLayer 的 attackButtonClick,别被 攻击按钮的 _delegate 和 HudLayer 的 _delegate 相通方法名 attackButtonClick 给骗了,它们完全可以不同,所以说是直接的。

我们已经注意到,在 HudLayer 虽然实现了调用 _delegate 的 attackButtonClick 方法,但是并没有看见摇杆控制的调用,如 actionJoypadStart 方法。这是因为在 HudLayer 的 setDelegate 方法中,在设置其本身 _delegate 的时候,也同样将它通过 this.mJoypad.setDelegate(delegate); 设置给了 Jodpad,这就意味着,在 Joypad 中,我们可以通过调用它的 _delegate 来控制 GameLayer 的操作,它将直接作用于 GameLayer,由 HudLayer 间接的传递委托(js 中没有明文的规定,所以你可以任意的将委托传递,这同样也是 js 的灵活之处,如果运用得当:否则不应该称之它为“灵活”了)。

HudLayer 的存在是为了托管控制操作,以便很容易的进行控制或者扩展,我们当然可以将所有的内容都一股脑的放在 GameLayer 中去实现,让它去接受触摸事件,去判断点击,去进行游戏的控制,但那样做,不利于我们后期的维护与扩展。例如,我想通过扩展,实现键盘操作控制,再如在游戏中,我可以通过网络发送命令来控制主角的走动,而对于 GameLayer 来说,显然它可以不用知道谁控制它,全部交由 HudLayer 来完成,下面对游戏进行简单的扩展,实现通过键盘来操作游戏。

操作控制之键盘映射

在使用键盘之前,我们需要先检测键盘是否可用,并且启用它,后通过一个数组(更准确的说是字典)来保存按键信息:

// GameConfig.js

// AC.KEYS 的定义

varAC = AC || {};

AC.KEYS = [];

// GameLayer.js init 方法

if(sys["capabilities"].hasOwnProperty('keyboard'))

this.setKeyboardEnabled(true);

// 保存按键信息

onKeyDown:function(e){

// 保存所有的按键信息

AC.KEYS[e] = true;

},

onKeyUp:function(e){

AC.KEYS[e] = false;

},

通过以上方式,将按键的信息保存在了 AC.KEYS 里面,用以在任何失去判断我们关系的按键是否被按下,以便完成一些操作。

varKeyMap = cc.Layer.extend({

_delegateJoypad: null,

_delegateAttack: null,

_pJoyKeyDown: false,

_pJKeyDown: false,

_pUKeyDown: false,

_pIKeyDown: false,

init:function(){

this._super();

this.scheduleUpdate();

returntrue;

},

setDelegateJoypad:function(delegate){

this._delegateJoypad = delegate;

},

setDelegateAttack:function(delegate){

this._delegateAttack = delegate;

},

update:function(dt){

this._super();

// 控制杆键盘映射处理

varau =false;

varal =false;

varad =false;

varar =false;

// 属性值的判断操作

// ...

varnewDegrees = -1;

// 通过按键判断摇杆方向,具体实现细节请看源码

if(this._delegateJoypad){

if(au || al || ad || ar){

if(!this._pJoyKeyDown)

this._delegateJoypad.keyStart(newDegrees);

this._pJoyKeyDown =true;

}

elseif(this._pJoyKeyDown){

this._pJoyKeyDown =false;

this._delegateJoypad.keyEnded(newDegrees);

}

if(newDegrees != -1 &&this._pJoyKeyDown){

this._delegateJoypad.keyUpdate(newDegrees);

}

}

// 攻击按钮控制映射

varkeyJ =false;

varkeyU =false;

varkeyI =false;

if(AC.KEYS[cc.KEY.j])

keyJ = true;

if(AC.KEYS[cc.KEY.u])

keyU = true;

if(AC.KEYS[cc.KEY.i])

keyI = true;

varpressJ =false;

varpressU =false;

varpressI =false;

if(keyJ){

if(!this._pJKeyDown){

this._pJKeyDown =true;

pressJ = true;

}

}else{

if(this._pJKeyDown){

// 发送一个攻击键松开的操作

this._delegateAttack.keyAttackUp(AT.ATTACK);

}

this._pJKeyDown =false;

}

// 其它按键的判断

// ...

if(this._delegateAttack){

if(pressJ)

this._delegateAttack.keyAttack(AT.ATTACK);

if(pressU)

this._delegateAttack.keyAttack(AT.ATTACK_A);

if(pressI)

this._delegateAttack.keyAttack(AT.ATTACK_B);

}

}

});

以上,我们通过 _delegateJoypad 和 _delegateAttack 来分别完成不同功能的调用,至于调用时机,由内部判断完成,也就是根据按钮的点击做出相应的响应,再作用于两个 delegate。

// 在  HudLayer 中 init 方法添加如下:

varkeyMap = KeyMap.create();

keyMap.setDelegateJoypad(this.mJoypad);

keyMap.setDelegateAttack(this);

this.addChild(keyMap);

// HudLayer 添加实现

keyAttack:function(btnType){

if(btnType == AT.ATTACK_A &&this.mAttackA.isCanClick())

this.mAttackA.click();

if(btnType == AT.ATTACK_B &&this.mAttackB.isCanClick())

this.mAttackB.click();

if(btnType == AT.ATTACK &&this.mAttack.isCanClick())

this.mAttack.click();

// this.attackButtonClick(bunType);

},

keyAttackUp:function(btnType){

if(btnType == AT.ATTACK)//  && this.mAttack.isCanClick())

this.mAttack.clickUp();

}

而 _delegateJoypad.keyStart 等方法的实际调用则在 Joypad 中完成,从而在去控制 GameLayer 的操作。在添加这样一个新的功能时,我们并没有对 GameLayer 进行太大的修改,只是在已有的实现,多添加一种触发条件而已,让 KeyMap 去调用 HudLayer 的 keyAttack,并且判断实际的攻击按钮,最终再去调用按钮的 click,完成按钮操作所应有的功能,如按钮再点击时的一些特效之类。而 Joypad 的调用异曲同工 ~

用户信息更新

为用户添加状态信息,如角色名称,血条等,我们最终显示的效果是这样的:

88dc4913d14d6c7c3e3d25bbbb4a0f68.png

为此我们需要准备一系列素材。

36de9b71d09e14c3c2210b757c687833.png

开发之时,这里只实现了,血条的改变,而并没有添加其它 值 的状态改变,但这并不影响实现它们。对于英雄和机器人(系统人物)来说,它们都有血量状态,都能改变,为此,设定一个抽象的数据类型来标示状态,让代码得到重用:

varState = cc.Node.extend({

_bloodSprite: null,

_roleType: null,

ctor:function(){

this._super();

},

setBloodSprite:function(obj){

this._bloodSprite = obj;

},

init:function(){

varbRet =false;

if(this._super()){

cc.NotificationCenter.getInstance().addObserver(this,this.notifyChangeStatus,"status",null);

bRet = true;

}

returnbRet;

},

notifyChangeStatus:function(obj){

if(obj.getRoleType() ==this._roleType){

cc.log("notify status ...");

this.setBlood(obj.getBloodPercent());

}

},

setBlood:function(value){

// 显示血量百分比

if(value 

value = 0;

if(value > 1)

value = 1;

this._bloodSprite.setScaleX(value);

},

setRoleType:function(type){

this._roleType = type;

}

});

State.create = function(){

varstate =newState();

if(state && state.init()){

returnstate;

}

returnnull;

};

这里使用通知机制,来完成对状态的更新,需要注意的是,在当前使用的版本 H5 – 2.1.5 中,默认并没有启用 NotificationCenter,你可以将 “[H5]/cocos2d/support/CCNotificationCenter.js” 添加到 “[H5]/cocos2d/CCLoader.js” 中去。

State.createHero =function(){

varstate = State.create();

vars1 = cc.Sprite.create(s_HeroState1);

vars2 = cc.Sprite.create(s_HeroState2);

vars3 = cc.Sprite.create(s_HeroState3);

vars4 = cc.Sprite.create(s_HeroState4);

s1.setPosition(cc.p(-80, 3));

s2.setPosition(cc.p(33, 15));

s3.setPosition(cc.p(-45, -12));

s3.setAnchorPoint(cc.p(0, 0));

state.setBloodSprite(s3);

state.addChild(s1);

state.addChild(s2);

state.addChild(s3);

state.addChild(s4);

state.setRoleType(AC.ROLE_HERO);

vartitle = cc.LabelTTF.create("Lv7 一叶","Tahoma", 14);

title.setPosition(cc.p(-15, 30));

state.addChild(title);

returnstate;

};

State.createRobot = function(){

varstate = State.create();

vars1 = cc.Sprite.create(s_RobotState1);

vars2 = cc.Sprite.create(s_RobotState2);

vars3 = cc.Sprite.create(s_RobotState3);

vars4 = cc.Sprite.create(s_RobotState4);

s1.setPosition(cc.p(50, -16));

state.setBloodSprite(s1);

s1.setAnchorPoint(cc.p(1, 0));

// s1.ignoreAnchorPointForPosition(true);

s2.setPosition(cc.p(-20, -7));

s4.setPosition(cc.p(65, 1));

state.setRoleType(AC.ROLE_ROBOT);

state.addChild(s2);

state.addChild(s1);

state.addChild(s3);

state.addChild(s4);

vartitle = cc.LabelTTF.create("Lv5 子龙山人","Tahoma", 14);

title.setPosition(cc.p(-15, 12));

state.addChild(title);

returnstate;

};

上面使用两个方法创建了玩家状态和机器人状态,两者有类似的功能,唯一需要注意的是,血量的锚点设置,基于百分比的血量展示,用基于百分比的放大缩小控制。设置好锚点才能保证显示的效果。

#p#副标题#e#

《Cocos2d-html5 王者之剑实现(3)》

游戏中人物的走动,跑动,攻击等动作是必不可少,实现它们的方法一般采用帧动画或者骨骼动画。在本文的两个角色里,一个采用帧动画,另一个采用骨骼动画(使用CocoStudio 的动画编辑器),同时也能很清楚的区别,两种方式的优劣,以及使用方式 ~ 有以下几个角度。

图片资源:首先对比一下使用帧动画和骨骼动画的所需要的图片资源。

31ada02ade7e5313e70f5162f053df63.png

如上图所示,角色英雄使用了帧动画(实际上图没有显示全,因为较多),他有各种动作,站立,跑动,攻击等效果,我们要为每一个动作创建几个“帧”,而动画的流畅性,取决于“帧数”的多少,但要知道,图片资源的大小也取决于你“帧数”的多少(浪费比特是不对的 ~),需要什么效果,需要多少帧,有多少动画,都需要自己权衡 ~

而怪物图片资源采用骨骼动画,资源是一块块小的“骨骼”,这无疑节省了资源大小,而动作信息则保存在一个 json 文件里面,后文会提到,而此时,随着动作的增加,所增加的比特(Byte)几可忽略不计。

使用方式:对于两者的使用方式,关键代码如下 :

// *********************帧动画加载与调用************************

// 动作的加载

initAction:function(){

// 站立动作

varsa = cc.Animation.create();

for(varsi = 1; si 

varframeName1 ="res/Hero"+ si +".png";

sa.addSpriteFrameWithFile(frameName1);

}

sa.setDelayPerUnit(5.8 / 14);

sa.setRestoreOriginalFrame(true);

this._actionStand = cc.RepeatForever.create(cc.Animate.create(sa));

// 跑动动作

varanimation = cc.Animation.create();

for(vari = 1; i 

varframeName ="res/HeroRun"+ i +".png";

animation.addSpriteFrameWithFile(frameName);

}

animation.setDelayPerUnit(2.8 / 14);

animation.setRestoreOriginalFrame(true);

this._actionRunning = cc.RepeatForever.create(cc.Animate.create(animation));

// 普通攻击

varanAttack = cc.Animation.create();

for(varattackIndex = 1; attackIndex 

varattackFrame ="res/HeroAttack"+ attackIndex +".png";

anAttack.addSpriteFrameWithFile(attackFrame);

}

anAttack.setDelayPerUnit(1.8 / 14);

// anAttack.setRestoreOriginalFrame(false);

this._actionAttack = cc.Animate.create(anAttack);

// 跳跃攻击 ...

// 突刺攻击 ...

// 其它动作,如果有 ~

}

// 动作的调用

this._sprite.runAction(this._actionStand);// 站立

this._sprite.runAction(this._actionRunning);// 跑动

// ...

// *********************骨骼动画加载与调用************************

// 加载骨骼资源

vars_Robot_png ="res/armature/Robot.png";

vars_Robot_plist ="res/armature/Robot.plist";

vars_Robot_json ="res/armature/Robot.json";

cc.ArmatureDataManager.getInstance().addArmatureFileInfo(

s_Robot_png,

s_Robot_plist,

s_Robot_json);

this._armature = cc.Armature.create("Robot");

// 使用方法

this._armature.getAnimation().play("stand");// 站立

this._armature.getAnimation().play("run");// 跑动

// ...

如上代码,对于动作的初始化,可以看到对于帧动画来说,非常繁琐,需要加载每一帧的图片,组合成一个动作动画,而骨骼动画则不然,资源的加载非常简单,调用方式也很简单。实际,在 CocoStudio 中也能够使用帧动画,并且使得动画的加载过程变得简单!

显示效果: 就显示效果而言,帧动画有如播放电影同样,只有帧率很高的时候才能达到不错的显示效果,然而骨骼动画,其帧率和游戏的帧率同样,唯一的区别,就是需要制作骨骼动画,但是对于这里英雄角色帧动画的制作过程而言,也是先制作成每一块骨骼,然后为每一帧调节其位置关系,并且少了骨骼节点,位置角度等也都不好控制。这里从制作过程到显示,都可以发现骨骼动画的优势所在。

a9a9aa7d1452baf8f9f0938000007948.png

0b2014deea37090524da68f4f2a7eb46.png

项目版本库升级

为了得到更好的骨骼动画支持,将这个小项目所使用的 H5 版本库,从 2.1.5 升级到了 2.1.6,这其中修复了些 骨骼动画中的 Bug,并且也对核心库的一些 API 做了修改,如:

// 新的触摸注册方式 ~

cc.Director.getInstance().getTouchDispatcher().addTargetedDelegate(this, 0,false);

cc.registerTargetedDelegate(0, true,this);

cc.Director.getInstance().getTouchDispatcher().removeDelegate(this);

cc.unregisterTouchDelegate(this);

// 精灵翻转

// 2.1.5

sprite.setFlipX

// 2.1.6

sprite.setFlippedX

角色动作组织

游戏中,对英雄和怪物来说,有一些通用的方法或者代码结构,为此提取出一个 ActionSprite 以标示这样一个角色:

varActionSprite = cc.Node.extend({

// 初始化方法

init:function(obj){...},

// 攻击

acceptAttack:function(obj){...},

// 是否翻转,图片“左右”走动

isFlip:function(){...},

// 设置精灵

setSprite:function(image, pos){...},

// 开始跑动 附带方向,方向是一个小于 360 的角度

runWithDegrees:function(degrees){...},

// 跑动,改变方向

moveWithDegrees:function(degrees){...},

// 停止跑动

idle:function(){...},

// 每帧更新

update:function(dt){...},

// 简单 ai 实现

ai:function(){...},

// 屏幕检测,人物不能走出屏幕之外 并且只能在下方

checkLocation:function(){...},

// 站立

hStand:function(){...},

// 跑动

hRunning:function(){...},

// ...

});

对于英雄和怪物的实现来说,有所不同,除了前文中繁杂的动作初始化方法之外,其它实现如下:

hAttack:function(at){

varaa =null;

if(at == AT.ATTACK){

aa = this._actionAttack;

this._attackRangt = 150;

}elseif(at == AT.ATTACK_A){

aa = this._actionAttackJump;

// 当前位置跳跃

varjump = cc.JumpTo.create(

0.6, cc.pSub(this.getPosition(), cc.p(this._flipX ? 200: -200)), 120, 1);

this.runAction(jump);

this._attackRangt = 300;

}elseif(at == AT.ATTACK_B){

aa = this._actionAttackT;

// 当前位置移动

varmove = cc.MoveTo.create(0.3, cc.pSub(this.getPosition(), cc.p(

this._flipX ? 200:-200, 0)));

this.runAction(move);

this._attackRangt = 300;

}

if(aa){

this._sprite.stopAllActions();

varaction = cc.Sequence.create(

aa,

cc.CallFunc.create(this.callBackEndAttack,this));

this._sprite.runAction(action);

this._state = AC.STATE_HERO_ATTACK;

this.postAttack();

}

},

attack:function(at){

this.hAttack(at);

},

callBackEndAttack:function(){

if(this._isRun){

this.hRunning();

}else{

this.hStand();

}

}

对于怪物,实现如下:

varRobot = ActionSprite.extend({

_armture:null,

init:function(){

varbRet =false;

if(this._super()){

cc.ArmatureDataManager.getInstance().addArmatureFileInfo(

s_Robot_png,

s_Robot_plist,

s_Robot_json);

this._armature = cc.Armature.create("NewProject");

this.setSprite(this._armature, cc.p(500, 300));

this.setZLocatoin(-90);

this.hStand();

this.runWithDegrees(180);

this.setRoleType(AC.ROLE_ROBOT);

this._imageflipX =true;

bRet = true;

this._speed = 150;

}

returnbRet;

},

setSprite:function(armature, pos){

this._sprite = armature;

this.addChild(this._sprite);

this.setPosition(pos);

},

hAttack:function(at){

this._attackRangt = 150;

this._sprite.stopAllActions();

this._sprite.getAnimation().play("attack");

this._sprite.getAnimation().setMovementEventCallFunc(this.callBackEndAttack,this);

this._state = AC.STATE_HERO_ATTACK;

this.postAttack();

},

hStand:function(){

this._sprite.getAnimation().play("stand");

this._state = AC.STATE_HERO_STAND;

},

hRunning:function(){

this._sprite.getAnimation().play("run");

this._state = AC.STATE_HERO_RUNNING;

},

attack:function(button){

this.hAttack(button);

},

callBackEndAttack:function(armature, movementType, movementID){

if(movementType == CC_MovementEventType_LOOP_COMPLETE) {

if(this._isRun){

this.hRunning();

}else{

this.hStand();

}

}

},

_timestamp: (newDate()).valueOf(),

_attackIndex: 0,

_moveIndex: 0,

ai:function(){

varnewTs = (newDate()).valueOf();

varvalue = newTs -this._timestamp;

if(this._moveIndex 

this._moveIndex += 1;

varr = Math.random() * 360;

this.moveWithDegrees(r);

}

if(this._attackIndex 

this._attackIndex += 1;

this.attack();

}

}

});

看到上面代码中的最后一小段,一个不是 AI 的 AI,每三秒钟做一次随机方向的走动,每六秒钟做一次攻击操作。

攻击判断:我们知道,只有在英雄和怪物站在一起时,才能攻击的到,表现在游戏画面中,那便是脚部所在的位置,在同一个 Y 坐标上,或者 Y 坐标的值在一个范围之内才能有效,所以在初始化的时候,设定了一个属性来标示它 (setZLocation),在攻击的时候,会去判断它们是否在有效的 Y 坐标之内,如下图中脚下的黄色线条(这线条素材不过时其它素材借来一用而已:D),除了上下位置关系的判断,当然也还有距离判断,则在代码中的 ActionSprite 实现:

a92f70c705778cce9ea93784c5105992.png

在攻击之时,攻击者,发送一个消息,所有的可被攻击者都会收到这个消息,然后判断是否被攻击到,而后做相应的操作,如掉血等 ~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值