跑酷》Starter Kit

《跑酷》Starter Kit

Read as other languages:英语

《跑酷》Starter Kit

翻译:Jack★魏凡缤,紫夜行者,夜狼。  校对:u0u0

Parkour Game Starter Kit

介绍

跑酷是一种非常受大众欢迎的游戏。这款入门套件教你如何使用Cocos2D-x以及相关工具创建有趣的跑酷游戏,而且同时支持iOS和Android。

一图胜过千言万语,让我们先看看学完这个教程后最终的游戏接图:

《跑酷》Starter Kit

在这个游戏里面, 我们使用手势控制。 向上滑动跳起,向下滑动蹲下。逆时针画圈开启无敌模式。

入门套件使用cocos2d-x javascript绑定实现。

本文描述的步骤基于Mac OS X和Xcode开发环境实现。

关于作者

《跑酷》Starter Kit

KeNan Liu 是一个开发者和ityran.com联合创始人。七年移动软件开发经验,涉及多个开发平台,如Windows Mobile, Brew, iOS and Windows Phone 8。现专注基于Cocos2dx游戏开发。你可以在Weibo上关注他。

《跑酷》Starter Kit

Iven Yang 当前专注于cocos2d-x开发的工程师,同时也是泰然团队的联合创始人。

关于编辑

《跑酷》Starter Kit

Yiming Guo 是一个对移动网络、云计算和数据挖掘感兴趣的在读本科生。现在在成都实习,专注于Cocos2d-x游戏开发。

关于美术

《跑酷》Starter Kit

Fan Wang 有四年的美术设计经验。

Chapter 1: Getting Started

Note: 如果你已经熟悉如何创建多平台cocos2d-x项目可以跳到下个部分。

Get Cocos2d-x

打开网页地址Cocos2D-x download page.

有几个多个cocos2d-x可选择下载。建议下载最新的稳定版本。写这个starter kit的时候是cocos2d-x-2.1.5。

Creating a multi-platform project of Cocos2d-x

打开 Terminal 终端. 使用cd 命令跳转到你解压cocos2d-x的目录, 如下:

   
   
  1. cd ~/Documents/project/cocos2d-x-2.1.4/tools/project-creator

Creating project use create_project.py

   
   
  1. ./create_project.py -project Parkour -package org.cocos2d-x.Parkour -language javascript

看见下列信息表示创建成功.

   
   
  1. proj.ios : Done!
  2. proj.android : Done!
  3. proj.win32 : Done!
  4. New project has been created in this path: /Users/u0u0/Documents/project/cocos2d-x-2.1.4/projects/Parkour
  5. Have Fun!

如上所示, create_project.py 自动生成IOS Android win32 项目。 这里我们用IOS项目做为例子。
跳转到项目目录打开项目。

   
   
  1. cd ~/Documents/project/cocos2d-x-2.1.4/projects/Parkour/proj.ios
  2. open Parkour.xcodeproj

项目创建完成,下面开始写代码。

Chapter 2: Setting up Multi-Resolution support

Cocos2d-x 提供一系列API使得游戏运行在不同分辨率下。技术细节参照这个文档。

Cocos2d-x 多分辨率适配完全解析

本文档展示如何使用它。

从HelloCpp 复制AppMacros.h 到Parkour项目

   
   
  1. cp ~/Documents/project/cocos2d-x-2.1.4/samples/Cpp/HelloCpp/Classes/AppMacros.h ~/Documents/project/cocos2d-x-2.1.4/projects/Parkour/Classes

拖动 AppMacros.h 到Classes文件夹。 弹出窗空中勾选“Add to targets”点击确认。

打开AppDelegate.cpp,添加AppMacros.h到文件顶部上。

   
   
  1. #include "AppMacros.h"

用下面的代码替换applicationDidFinishLaunching的函数实现:

   
   
  1. // initialize director
  2. CCDirector *pDirector = CCDirector::sharedDirector();
  3. CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();
  4. pDirector->setOpenGLView(pEGLView);
  5. // Set the design resolution
  6. pEGLView->setDesignResolutionSize(designResolutionSize.width, designResolutionSize.height, kResolutionFixedHeight);
  7. CCSize frameSize = pEGLView->getFrameSize();
  8. vector<string> searchPath;
  9. float mediumGap = (mediumResource.size.height - smallResource.size.height) / 2;
  10. if (frameSize.height > (smallResource.size.height + mediumGap)) {
  11. searchPath.push_back(mediumResource.directory);
  12. pDirector->setContentScaleFactor(mediumResource.size.height/designResolutionSize.height);
  13. } else {
  14. searchPath.push_back(smallResource.directory);
  15. pDirector->setContentScaleFactor(smallResource.size.height/designResolutionSize.height);
  16. }
  17. // set searching path
  18. CCFileUtils::sharedFileUtils()->setSearchPaths(searchPath);
  19. // turn on display FPS
  20. pDirector->setDisplayStats(true);
  21. // set FPS. the default value is 1.0/60 if you don't call this
  22. pDirector->setAnimationInterval(1.0 / 60);
  23. ScriptingCore* sc = ScriptingCore::getInstance();
  24. sc->addRegisterCallback(register_all_cocos2dx);
  25. sc->addRegisterCallback(register_all_cocos2dx_extension);
  26. sc->addRegisterCallback(register_cocos2dx_js_extensions);
  27. sc->addRegisterCallback(register_all_cocos2dx_extension_manual);
  28. sc->addRegisterCallback(register_CCBuilderReader);
  29. sc->addRegisterCallback(jsb_register_chipmunk);
  30. sc->addRegisterCallback(jsb_register_system);
  31. sc->addRegisterCallback(JSB_register_opengl);
  32. sc->addRegisterCallback(MinXmlHttpRequest::_js_register);
  33. sc->start();
  34. CCScriptEngineProtocol *pEngine = ScriptingCore::getInstance();
  35. CCScriptEngineManager::sharedManager()->setScriptEngine(pEngine);
  36. ScriptingCore::getInstance()->runScript("MainScene.js");

我们用了2中不同的资源区适配不同分辨率,小资源放在“iphone”目录,大资源放在“ipad”。

Note: 我们改变了js入口到MainScene.js。把 main.js 重新命名MainScene.js。 在Resource目录下的“res” 和“src”的不会在Parkour里面使用,删除它们然后点“Move to trash”。

创建2个目录拖拽它们到Resource文件夹,弹出框点“Create folder references for any added folders” 然后选“Add to targets”。

《跑酷》Starter Kit

项目Parkour的Resource现在长这样:

《跑酷》Starter Kit

Parkour 设计分辨率480*320, 所有图片会放在“iphone”下面,用iPhone模拟器运行. 完成所有游戏逻辑后我们添加其他的资源然后再不同分辨率下测试。

Chapter 3: Adding a Main Menu to Main Scene

现在我们有一个干净的Cocos2d­x JSB项目,入口是“MainScene.js”。还需要添加其他东西到“MainScene.js”确保其运行。

打开“MainScene.js” 用下列内容替换:

   
   
  1. // 1.
  2. require("jsb.js");
  3. // 2.
  4. var MainLayer = cc.Layer.extend({
  5. // 3.
  6. ctor:function () {
  7. this._super();
  8. this.init();
  9. },
  10. // 4.
  11. init:function () {
  12. this._super();
  13. var centerPos = cc.p(winSize.width / 2, winSize.height / 2);
  14. var spriteBG = cc.Sprite.create("MainBG.png");
  15. spriteBG.setPosition(centerPos);
  16. this.addChild(spriteBG);
  17. cc.MenuItemFont.setFontSize(60);
  18. var menuItemPlay = cc.MenuItemFont.create("Play", this.onPlay, this);
  19. var menu = cc.Menu.create(menuItemPlay);
  20. menu.setPosition(centerPos);
  21. this.addChild(menu);
  22. },
  23. // on play button clicked
  24. onPlay:function (sender) {
  25. // 5.
  26. log("==onPlay clicked");
  27. }
  28. });
  29. // 6.
  30. MainLayer.scene = function () {
  31. var scene = cc.Scene.create();
  32. var layer = new MainLayer();
  33. scene.addChild(layer);
  34. return scene;
  35. };
  36. // main entry
  37. try {
  38. // 7.
  39. director = cc.Director.getInstance();
  40. winSize = director.getWinSize();
  41. // run first scene
  42. director.runWithScene(MainLayer.scene());
  43. } catch(e) {log(e);}

要点解析:

1. Require() 会加载一个js module, 用文件名字作为形参。如果需要用cocos2d­x jsb 开发游戏 “jsb.js” 是一个必须被加载的module。 一个module一旦运行被加载,可以被使用在任何地方。

2. MainLayer = cc.Layer.extend() 是Cocos2d­x jsb 继承object的方式,源自John Resig’s javascript Inheritance。这里我们从CCLayer继承了一个新的类MainLayer。

3. Ctor()会被调用如果new一个MainLayer. 它是jsb的构造函数。 如果你重写这个方法记得调用this.super()。

4. 重写init()也需要调用this.super(). 我们用之前添加的背景图片创建一个精灵,放置在屏幕中间,作为子节点添加到MainLayer. 创建一个包含“Play”的菜单,并设置回调函数onPlay().

5. 目前onPlay()里面仅有打印一条log,后面我们再实现其功能。

6. MainLayer.scene = function (){}; 给MainLayer添加一个静态方法。

7. 是时候加在MainLayer了。用cc.Director.getInstance()获取director, 告诉director运行第一个场景。

Note:Director 和 winSize 被声明为全局变量,两个都被频繁使用。

运行项目将看到下面的画面:

《跑酷》Starter Kit

Chapter 4: PlayScene Overview

主场景有3个层。

《跑酷》Starter Kit

PlayLayer

该层有主角,地图,金币,岩石。

主角前进,玩家层镜头也前进。以确保主角始终在视野内。

背景有两个水平地图,主角从第一张地图移动到第二张地图时候,第一张地图自动加载到第二 张地图右侧,一直循环下去。

StatusLayer

状态层在玩家层上,金币和距离数据显示在该层。
为什么分层呢?

如果金币距离数据显示在玩家层,数据会在镜头移动时候消失,分层能简单解决这个问题。

GameOverLayer

该层是一个color layer.

主角撞上石头后显示游戏结束, 并提供一个replay按钮。

Chapter 5: Setting Up PlayLayer with Physics World

PlayLayer是PlayScene最重要的层。这层处理玩家输入,碰撞检测,物体运动等等。

创建个js文件,并且添加Xcode 项目

首先,在资源目录中创建一个名为PlayScene.js的文件并且拖拽到Xcode工程源文件夹里。在弹出的对话框中,确保Add to targets选中,然后单击Finish。

然后,在Xcode项目里选择目标,切换到“Build Phases”标签,展开“Copy bundle Resource”项目。

《跑酷》Starter Kit

滚动到底部并点击“+”。在弹出的对话框中,选择“PlayScene.js”,然后单击Add。

《跑酷》Starter Kit

Note: ios工程里添加js文件到“Copy bundle Resources”是一个必要的步骤。如果没有添加,当调用js时,你会看到下面的错误信息。

   
   
  1. Cocos2d: Get data from file(PlayScene.jsc) failed!
  2. Cocos2d: JS: /Users/u0u0/Library/Application Support/iPhone Simulator/6.1/Applications/3F9658F6-12CB-422A-89E9-6719D04B4D4B/Parkour.app/MainScene.js:3:Error: can't open PlayScene.js: No such file or directory

PlayLayer with Physics World
打开“PlayScene.js” 并且代替成下面的文本:

   
   
  1. var PlayLayer = cc.Layer.extend({
  2. // 1.
  3. space:null,// chipmunk space
  4. // constructor
  5. ctor:function () {
  6. this._super();
  7. this.init();
  8. },
  9. init:function () {
  10. this._super();
  11. this.initPhysics();
  12. // 2.
  13. this.scheduleUpdate();
  14. },
  15. // 3.
  16. initPhysics:function() {
  17. // 4.
  18. this.space = new cp.Space();
  19. // 5.
  20. this.space.gravity = cp.v(0, -350);
  21. // 6. set up Walls
  22. var wallBottom = new cp.SegmentShape(this.space.staticBody,
  23. cp.v(0, g_groundHight),// start point
  24. cp.v(4294967295, g_groundHight),// MAX INT:4294967295
  25. 0);// thickness of wall
  26. this.space.addStaticShape(wallBottom);
  27. },
  28. update:function (dt) {
  29. // 7.
  30. this.space.step(dt);
  31. }
  32. });
  33. PlayLayer.scene = function () {
  34. var scene = cc.Scene.create();
  35. var layer = new PlayLayer();
  36. scene.addChild(layer);
  37. return scene;
  38. };

一些重要的注意事项:

1. 定义个类成员变量。左边是变量名称,右边是变量初始值。

2. 启动“update”方法。

3. 这款游戏,我们使用Chipmunk2D物理引擎。Cocos2d-x种有两套Chipmunk JSB API。一个是面向对象的,另一个是面向过程的。我们使用更加友好的面向对象接口。

4. new cp.Space() 是面向对象的chipmunk API,用来创建一个物理世界。

5. 设置物理世界的重力。cp.v()等同于cc.p().

6. 跑酷所用的地面,chipmunk中使用静态形状来描述。从物理空间新建一个静态SegmentShape,然后将它添加到物理空间。

7. update()方法每帧被调用。我们在这里调用chipmunk setp方法是物理世界动起来。

全局变量 g_groundHight 定义在”Utils.js”文件里面。

   
   
  1. var g_groundHight = 50;

为了加载PlayScene, 我们需要打开”MainScene.js”, 添加下面的代码到头部。

   
   
  1. require("Utils.js");
  2. require("PlayScene.js");

用下面的代码替换onPlay的实现:

   
   
  1. onPlay:function (sender) {
  2. cc.Director.getInstance().replaceScene(PlayLayer.scene());
  3. }

调试并运行,点击“Play”按钮,屏幕将会显示一片黑。在下一章我们将添加些东西。

Chapter 6: Running This Way

本章我们将添加一个精灵到PlayLayer,并让他跑起来。我们称呼这个精灵为runner。

要实现跑的动作,我们需要帧动画。动画的实现要感谢资源文件下的精灵表单。

精灵表包括parkour.plist 和 parkour.png. 使用TexturePacker工具生成。

Note: 精灵表有助于减少内存消耗,加快绘图过程和保持帧率高。更多信息参考:精灵表单

帧动画由多张图片组成。

如下所示:

《跑酷》Starter Kit

把所有的图片拖到TexturePacker里。然后点击”Publish to output the sprite sheet”.如需使用 TexturePacker可以在它的官网上找到。

现在我们获得两个文件“parkour.plist”和“parkour.png”, 把他们移动到资源目录下的Resource/iphone里面.

创建一个名为”Runner.js”js文件然后如我们前面做的那样添加到Xcode工程里。内容如下:

   
   
  1. // 1.
  2. if(typeof RunnerStat == "undefined") {
  3. var RunnerStat = {};
  4. RunnerStat.running = 0;
  5. };
  6. // 2.
  7. var Runner = cc.Node.extend({
  8. sprite:null,
  9. runningSize:null,
  10. space:null,
  11. body:null,// current chipmunk body
  12. shape:null,// current chipmunk shape
  13. stat:RunnerStat.running,// init with running status
  14. runningAction:null,
  15. spriteSheet:null,
  16. get offsetPx() {return 100;},
  17. // 3.
  18. ctor:function (spriteSheet, space) {
  19. this._super();
  20. this.spriteSheet = spriteSheet;
  21. this.space = space;
  22. this.init();
  23. },
  24. init:function () {
  25. this._super();
  26. // 4.
  27. this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("runner0.png");
  28. this.runningSize = this.sprite.getContentSize();
  29. // 5.
  30. this.initAction();
  31. // 6.
  32. this.initBody();
  33. // 7.
  34. this.initShape();
  35. // 8.
  36. this.sprite.setBody(this.body);
  37. // 9.
  38. this.sprite.runAction(this.runningAction);
  39. // 10.
  40. this.spriteSheet.addChild(this.sprite, 1);
  41. // 11.
  42. this.stat = RunnerStat.running;
  43. },
  44. // 12.
  45. onExit:function() {
  46. this.runningAction.release();
  47. this._super();
  48. },
  49. // 13.
  50. getPositionX:function () {
  51. return this.sprite.getPositionX();
  52. },
  53. initAction:function () {
  54. // init runningAction
  55. var animFrames = [];
  56. // num equal to spriteSheet
  57. for (var i = 0; i < 8; i++) {
  58. var str = "runner" + i + ".png";
  59. var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
  60. animFrames.push(frame);
  61. }
  62. var animation = cc.Animation.create(animFrames, 0.1);
  63. this.runningAction = cc.RepeatForever.create(cc.Animate.create(animation));
  64. this.runningAction.retain();
  65. },
  66. initBody:function () {
  67. // create chipmunk body
  68. this.body = new cp.Body(1, cp.momentForBox(1,
  69. this.runningSize.width, this.runningSize.height));
  70. this.body.p = cc.p(this.offsetPx, g_groundHight + this.runningSize.height / 2);
  71. this.body.v = cp.v(150, 0);//run speed
  72. this.space.addBody(this.body);
  73. },
  74. initShape:function (type) {
  75. this.shape = new cp.BoxShape(this.body,
  76. this.runningSize.width, this.runningSize.height);
  77. this.space.addShape(this.shape);
  78. },
  79. });

一些重要的注意事项:

1. JS的方式来定义一个runner状态的枚举。跑步者有许多状态,但对于这一章中,我们只关心的运行状态。

2. cc.PhysicsSprite 没有扩展方法。所以Runner类从cc.Node里扩展。

3. 跑步者将被创建并放置在物理世界的PlayLayer里。所以在构造函数里面引用物理空间和精灵表单。

4. 在你调用cc.PhysicsSprite.createWithSpriteFrameName创建物理精灵之前,你需要在内存 里初始化通过TexturePacker创建的精灵表。这部分工作将在PlayLayer完成。“runner0.png”是第一帧图片。

5. 在initAction里, 从帧缓存创建一个帧动画, 让这个动画循环播放。注意 “this.runningAction.retain()”这行代码­­­­­retain()将避免CCObject被GC。

6. 在initBody里, 创建runner的物理body, 并设置初始速度。

7. 在initShape里,创建与精灵大小相等的chipmunk形状。

8. 让物理引擎对象和精灵对象关联起来。

9. 让精灵播放动画。

10. 精灵添加到精灵表单的子节点。

11. 记录状态。我们在后面章节将会使用。

12. 重写onExit释放runningAction。如果你重写这个方法,记住调用this._super()。

13. 这个助手函数在PlayLayer里用于计算相机的移动。

切换到PlayScene.js添加下面的代码到文件头部:

   
   
  1. require("Runner.js");

定义新的类成员变量。

   
   
  1. spriteSheet:null,
  2. runner:null,
  3. lastEyeX:0,

跳转到init()函数做如下修改:

   
   
  1. // create sprite sheet of PlayLayer
  2. cc.SpriteFrameCache.getInstance().addSpriteFrames("parkour.plist");
  3. this.spriteSheet = cc.SpriteBatchNode.create("parkour.png");
  4. this.addChild(this.spriteSheet);
  5. this.runner = new Runner(this.spriteSheet, this.space);
  6. // runner is base on Node, addChild to make scheduleOnce and onExit call.
  7. this.addChild(this.runner);

然后跳转到update()函数做如下修改:

   
   
  1. // move Camera
  2. this.lastEyeX = this.runner.getPositionX() - this.runner.offsetPx;
  3. var camera = this.getCamera();
  4. var eyeZ = cc.Camera.getZEye();
  5. camera.setEye(this.lastEyeX, 0, eyeZ);
  6. camera.setCenter(this.lastEyeX, 0, 0);

runner的新位置回由物理世界在每帧计算,相机需要跟随runner的移动步伐。

编译并运行这个程序,然后你可以看到一个男孩在屏幕上运行。

Chapter 7: Gesture Recognizer

到目前为止runner可以向前移动。在给runner添加用户控制前,你需要处理玩家的输入。

在游戏中我们用跳上、跳下和转圈这三个手势来控制runner。

$1 Unistroke Recognizer是一个开源库。支持包含花圈在内的16个手势识别,有javaScript版本,可以很容易的导入到Cocos2d­x JSB项目里。

但是它有个缺点:很难区分向上滑动和向下滑动。必须由你自己去识别这两个手势。

Simple Recognizer

Simple Recognizer可以识别简单手势包括swipe up, swipe down, swipe left and swipe right.

创建一个名为“SimpleRecognizer.js”的js文件。替代内容如下:

   
   
  1. // 1.
  2. function Point(x, y)
  3. {
  4. this.X = x;
  5. this.Y = y;
  6. }
  7. // class define
  8. function SimpleRecognizer()
  9. {
  10. this.points = [];
  11. this.result = "";
  12. }
  13. SimpleRecognizer.prototype.beginPoint = function(x, y) {
  14. this.points = [];
  15. this.result = "";
  16. this.points.push(new Point(x, y));
  17. }
  18. SimpleRecognizer.prototype.movePoint = function(x, y) {
  19. this.points.push(new Point(x, y));
  20. if (this.result == "not support") {
  21. return;
  22. }
  23. var newRtn = "";
  24. var len = this.points.length;
  25. // 2.
  26. var dx = this.points[len - 1].X - this.points[len - 2].X;
  27. var dy = this.points[len - 1].Y - this.points[len - 2].Y;
  28. if (Math.abs(dx) > Math.abs(dy)) {
  29. // 3.
  30. if (dx > 0) {
  31. newRtn = "right";
  32. } else {
  33. newRtn = "left";
  34. }
  35. } else {
  36. // 4.
  37. if (dy > 0) {
  38. newRtn = "up";
  39. } else {
  40. newRtn = "down";
  41. }
  42. }
  43. // first set result
  44. if (this.result == "") {
  45. this.result = newRtn;
  46. return;
  47. }
  48. // if diretcory change, not support Recognizer
  49. if (this.result != newRtn) {
  50. this.result = "not support";
  51. }
  52. }
  53. SimpleRecognizer.prototype.endPoint = function(x, y) {
  54. if (this.points.length < 3) {
  55. return "error";
  56. }
  57. return this.result;
  58. }
  59. SimpleRecognizer.prototype.getPoints = function() {
  60. return this.points;
  61. }

注意以下重要事项:

1. 定义与dallar库一样的Point。这使得项目可以很简单的使用这两个库。

2. 每当触点移动时,在当前触点和之前触点之间计算不同的x坐标和y坐标。

3. 在这种情况下,运动趋势的触点在x轴方向。

4. 在这种情况下,运动趋势的触点在y轴方向。

$1 Unistroke Recognizer

开web浏览器,并导航到http://depts.washington.edu/aimgroup/proj/dollar/dollar.js. 保存到本地磁盘,并将其拖到资源目录下。添加dollar.js到Xcode项目作为第五章项目。

使用这个库之前你需要做一些优化。

在本库里包含16个手势。每次匹配,它必须遍历所有手势。注释掉无用的可以节省cpu时间。但是你不能注释掉所有不用的,否则每个识别结果将是“圆”。你需要保留一些干扰项。

开始优化吧。

打开dollar.js 并且修改NumUnistrikes值。

   
   
  1. var NumUnistrokes = 4;//16;

注释掉无用的但保留“三角形”、“圆”、“左方括号”和“右方括号”。修改这四个Unistrokes数组下标。

Integrated into the PlayLayer

切换到PlayScene.js,并且在文件的顶部添加下面内容:

   
   
  1. require("SimpleRecognizer.js");
  2. require("dollar.js");

定义新的类成员变量

   
   
  1. recognizer:null,
  2. dollar:null,

跳转到函数init()在this.initPhysics()后面添加下面的代码.

   
   
  1. // enable touch
  2. this.setTouchEnabled(true);
  3. // set touch mode to kCCTouchesOneByOne
  4. this.setTouchMode(1);
  5. this.dollar = new DollarRecognizer();
  6. this.recognizer = new SimpleRecognizer();

You enable the touch of the layer, and set touch mode to kCCTouchesOneByOne, which receive touch point one at a time in event callbacks.
打开这个层的触摸事件,并设置为一次只反馈一个触摸点的kCCTouchesOneByOne模式。

添加下面代码到PlayLayer:

   
   
  1. onTouchBegan:function(touch, event) {
  2. var pos = touch.getLocation();
  3. this.recognizer.beginPoint(pos.x, pos.y);
  4. return true;
  5. },
  6. onTouchMoved:function(touch, event) {
  7. var pos = touch.getLocation();
  8. this.recognizer.movePoint(pos.x, pos.y);
  9. },
  10. onTouchEnded:function(touch, event) {
  11. var rtn = this.recognizer.endPoint();
  12. switch (rtn) {
  13. case "up":
  14. log("==jumping");
  15. break;
  16. case "down":
  17. log("==crouching");
  18. break;
  19. case "not support":
  20. case "error":
  21. // try dollar Recognizer
  22. // 0:Use Golden Section Search (original)
  23. // 1:Use Protractor (faster)
  24. var result = this.dollar.Recognize(this.recognizer.getPoints(), 1);
  25. log(result.Name);
  26. if (result.Name == "circle") {
  27. log("==incredible");
  28. }
  29. break;
  30. }
  31. },
  32. onTouchCancelled:function(touch, event) {
  33. log("==onTouchCancelled");
  34. },

简单的识别器 识别 速度超过$1 Unistroke Recognizer。由它先识别swipe up 和 swipe down。 如果它不能识别,再使用$1 Unistroke Recognizer。
调试并运行,尝试swipe up, swipe down ,画一个圆。你将看到下面的日志。

   
   
  1. Cocos2d: JS: ==jumping
  2. Cocos2d: JS: ==crouching
  3. Cocos2d: JS: circle
  4. Cocos2d: JS: ==incredible

Chapter 8: Jumping and Crouching

这一节中将介绍如何添加一些跑酷游戏的常用的一些控制方式。

在修改runner类之前首先要在Utils.js中添加如下代码

   
   
  1. if(typeof SpriteTag == "undefined") {
  2. var SpriteTag = {};
  3. SpriteTag.runner = 0;
  4. SpriteTag.coin = 1;
  5. SpriteTag.rock = 2;
  6. };

需要定义一个标记chipmunk碰撞检测的枚举类型

打开Runner.js,通过添加如下代码来完成RunnerStat的定义:

   
   
  1. RunnerStat.jumpUp = 1;
  2. RunnerStat.jumpDown = 2;
  3. RunnerStat.crouch = 3;
  4. RunnerStat.incredible = 4;

为runner类定义几个新的成员变量

   
   
  1. crouchSize:null,
  2. jumpUpAction:null,
  3. jumpDownAction:null,
  4. crouchAction:null,

当游戏角色蹲下的时候它的形状将会发生改变,下面的代码会记录蹲下时候的大小。 在init()中添加如下代码:

   
   
  1. var tmpSprite = cc.PhysicsSprite.createWithSpriteFrameName("runnerCrouch0.png");
  2. this.crouchSize = tmpSprite.getContentSize();

改变

   
   
  1. this.initShape();

   
   
  1. this.initShape("running");

当然还要修改initShape()中的代码。用下面的代码替换它的内容:

   
   
  1. initShape:function (type) {
  2. if (this.shape) {
  3. this.space.removeShape(this.shape);
  4. }
  5. if (type == "running") {
  6. this.shape = new cp.BoxShape(this.body,
  7. this.runningSize.width, this.runningSize.height);
  8. } else {
  9. // crouch
  10. this.shape = new cp.BoxShape(this.body,
  11. this.crouchSize.width, this.crouchSize.height);
  12. }
  13. this.shape.setCollisionType(SpriteTag.runner);
  14. this.space.addShape(this.shape);
  15. },

还有initAction()中的三个动画初始化函数:jumpUpAction, jumpDownAction和crouchAction

   
   
  1. // init jumpUpAction
  2. animFrames = [];
  3. for (var i = 0; i < 4; i++) {
  4. var str = "runnerJumpUp" + i + ".png";
  5. var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
  6. animFrames.push(frame);
  7. }
  8. animation = cc.Animation.create(animFrames, 0.2);
  9. this.jumpUpAction = cc.Animate.create(animation);
  10. this.jumpUpAction.retain();
  11. // init jumpDownAction
  12. animFrames = [];
  13. for (var i = 0; i < 2; i++) {
  14. var str = "runnerJumpDown" + i + ".png";
  15. var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
  16. animFrames.push(frame);
  17. }
  18. animation = cc.Animation.create(animFrames, 0.3);
  19. this.jumpDownAction = cc.Animate.create(animation);
  20. this.jumpDownAction.retain();
  21. // init crouchAction
  22. animFrames = [];
  23. for (var i = 0; i < 1; i++) {
  24. var str = "runnerCrouch" + i + ".png";
  25. var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
  26. animFrames.push(frame);
  27. }
  28. animation = cc.Animation.create(animFrames, 0.3);
  29. this.crouchAction = cc.Animate.create(animation);
  30. this.crouchAction.retain();

已经完成了初始化,然后要做的是在Runner类中添加下面的函数:

   
   
  1. jump:function () {
  2. if (this.stat == RunnerStat.running) {
  3. this.body.applyImpulse(cp.v(0, 250), cp.v(0, 0));
  4. this.stat = RunnerStat.jumpUp;
  5. this.sprite.stopAllActions();
  6. this.sprite.runAction(this.jumpUpAction);
  7. }
  8. },

只需要给runner的body一个向上的冲力,runner就会跳起来,在将动画换成跳跃动画之前,通过调用sprite.stopAllActions()来停止当前动画。

跳跃的动作可以分为两个部分--上升和下降。可以通过观察body的重心的线速度来检测从上升到下降状态的转换。

如果在Y轴的线速度小于0.1,跳跃的动作正在从上升状态切换到下降状态,这时将精灵的动画切换到jumpDownAction.

如果Y轴的线速度等于0说明精灵的从下降的状态切换到了跑动的状态,这时将精灵动画改成runningAction.

这些工作将在sprite.step()中完成,代码如下:

   
   
  1. step:function (dt) {
  2. var vel = this.body.getVel();
  3. if (this.stat == RunnerStat.jumpUp) {
  4. if (vel.y < 0.1) {
  5. this.stat = RunnerStat.jumpDown;
  6. this.sprite.stopAllActions();
  7. this.sprite.runAction(this.jumpDownAction);
  8. }
  9. return;
  10. }
  11. if (this.stat == RunnerStat.jumpDown) {
  12. if (vel.y == 0) {
  13. this.stat = RunnerStat.running;
  14. this.sprite.stopAllActions();
  15. this.sprite.runAction(this.runningAction);
  16. }
  17. return;
  18. }
  19. },

在蹲下的时候,只需要修改body的shape.把下面的函数添加到Runner类中:

   
   
  1. crouch:function () {
  2. if (this.stat == RunnerStat.running) {
  3. this.initShape("crouch");
  4. this.sprite.stopAllActions();
  5. this.sprite.runAction(this.crouchAction);
  6. this.stat = RunnerStat.crouch;
  7. // after time turn to running stat
  8. this.scheduleOnce(this.loadNormal, 1.0);
  9. }
  10. },

蹲下的状态不会持续太长的时间,可以通过调用this.scheduleOnce(this.loadNormal, 1.0)来返回到跑动状态.

loadNormal() 初始化跑动状态下body的shape.可以这样做:

   
   
  1. loadNormal:function (dt) {
  2. this.initShape("running");
  3. this.sprite.stopAllActions();
  4. this.sprite.runAction(this.runningAction);
  5. this.stat = RunnerStat.running;
  6. },

现在已经完成了Runner.js,用下面代码替换PlayScene.js中的onTouchEnded函数:

   
   
  1. onTouchEnded:function(touch, event) {
  2. var rtn = this.recognizer.endPoint();
  3. switch (rtn) {
  4. case "up":
  5. this.runner.jump();
  6. break;
  7. case "down":
  8. this.runner.crouch();
  9. break;
  10. case "not support":
  11. case "error":
  12. // try dollar Recognizer
  13. // 0:Use Golden Section Search (original)
  14. // 1:Use Protractor (faster)
  15. var result = this.dollar.Recognize(this.recognizer.getPoints(), 1);
  16. log(result.Name);
  17. if (result.Name == "circle") {
  18. this.runner.incredibleHulk();
  19. }
  20. break;
  21. }
  22. },

添加下面代码来切换动画:

   
   
  1. // runner step, to change animation
  2. this.runner.step(dt);

编译运行之,上下滑动来看看跳跃和蹲下的效果。

Chapter 9: Map Loop

目前为止,游戏角色还很孤独的跑在一个黑色的世界,现在要做的是给游戏添加背景图片。 背景由上下两部分组成,当游戏角色在两张图片中间的夹缝上跑动的时候,第一张背景图片会慢 慢被第二张背景图片替换,第一张图片将重新加载。

下面代码是一个将整数转为一个特定长度的javascript函数,将以下代码添加到Utils.js文件中:

   
   
  1. function FormatNumberLength(num, length) {
  2. var r = "" + num;
  3. while (r.length < length) {
  4. r = "0" + r;
  5. }
  6. return r;
  7. }

创建一个名为Map.js的文件并添加到Xcode的项目中,添加下面的代码:

   
   
  1. require("Utils.js");
  2. var Map = cc.Class.extend({
  3. layer:null,
  4. space:null,
  5. spriteWidth:0,
  6. // 1.
  7. mapCount:2,// total map of resource
  8. map0:null,
  9. map1:null,
  10. ground0:null,
  11. ground1:null,
  12. curMap:0,// [0, n]
  13. ctor:function (layer, space) {
  14. this.layer = layer;
  15. this.space = space;
  16. // 2.
  17. this.map0 = cc.Sprite.create("Map00.png");
  18. this.map0.setAnchorPoint(cc.p(0, 0));
  19. this.map0.setPosition(cc.p(0, 0));
  20. this.layer.addChild(this.map0);
  21. // 3.
  22. this.ground0 = cc.Sprite.create("Ground00.png");
  23. this.ground0.setAnchorPoint(cc.p(0, 0));
  24. var size = this.ground0.getContentSize();
  25. this.ground0.setPosition(cc.p(0, g_groundHight - size.height));
  26. this.layer.addChild(this.ground0);
  27. this.spriteWidth = this.map0.getContentSize().width;
  28. this.map1 = cc.Sprite.create("Map01.png");
  29. this.map1.setAnchorPoint(cc.p(0, 0));
  30. // 4.
  31. this.map1.setPosition(cc.p(this.spriteWidth, 0));
  32. this.layer.addChild(this.map1);
  33. this.ground1 = cc.Sprite.create("Ground01.png");
  34. this.ground1.setAnchorPoint(cc.p(0, 0));
  35. this.ground1.setPosition(cc.p(this.spriteWidth, g_groundHight - size.height));
  36. this.layer.addChild(this.ground1);
  37. },
  38. getMapWidth:function () {
  39. return this.spriteWidth;
  40. },
  41. getCurMap:function () {
  42. return this.curMap;
  43. },
  44. checkAndReload:function (eyeX) {
  45. // 5.
  46. var newCur = parseInt(eyeX / this.spriteWidth);
  47. if (this.curMap == newCur) {
  48. return false;
  49. }
  50. var map;
  51. var ground;
  52. if (0 == newCur % 2) {
  53. // change mapSecond
  54. map = this.map1;
  55. ground = this.ground1;
  56. } else {
  57. // change mapFirst
  58. map = this.map0;
  59. ground = this.ground0;
  60. }
  61. log("==load map:" + (newCur + 1));
  62. this.curMap = newCur;
  63. // 6.
  64. var fileName = "Map" + FormatNumberLength((newCur + 1) % this.mapCount, 2) + ".png";
  65. var texture = cc.TextureCache.getInstance().addImage(fileName);
  66. map.setTexture(texture);
  67. map.setPositionX(this.spriteWidth * (newCur + 1));
  68. // load ground
  69. var fileName = "Ground" + FormatNumberLength((newCur + 1) % this.mapCount, 2) + ".png";
  70. var texture = cc.TextureCache.getInstance().addImage(fileName);
  71. ground.setTexture(texture);
  72. ground.setPositionX(this.spriteWidth * (newCur + 1));
  73. return true;
  74. },
  75. });

几个重点:

1. MapCount应等于在资源文件夹中的文件数量。并应不少于两个。

2. 地图的上半部分是一个锚点改为(0,0)的精灵,用更改锚点来简化坐标的计算

3. 上层部分跟下层部分的不同之处是它们的位置坐标,将下层部分Y轴坐标设置为 g_groundHight – this.ground0.getContentSize().height来确保游戏角色的脚是踏在地面上的。

4. 第二张地图的开始位置是背景的宽度

5. 用这种方式计算地图坐标

6. 在一张背景跑完之后需要切换一张新的图片

在PlayScene.js的前面添加如下代码:

   
   
  1. require("Map.js");

定义新的变量

   
   
  1. map:null,

通过添加下面代码到init()初始化地图:

   
   
  1. this.map = new Map(this, this.space);

将下面代码添加到update()函数中:

   
   
  1. // check and reload map
  2. if (true == this.map.checkAndReload(this.lastEyeX)) {
  3. //level up
  4. this.runner.levelUp();
  5. }

编译运行后你将看到这样的界面:

《跑酷》Starter Kit

Chapter 10: Adding Coins and Rocks

现在已经可以让游戏角色跑在一个有背景的世界了,但是要完成一个跑酷游戏的话还需要两个东西:金币和石头.

当游戏角色碰到了金币,金币将会消失但是石头不会消失,当他碰到石头,game over! 除了碰撞处理外它们没有任何区别,让我们从金币开始做吧。

创建一个Coin.js的javascript文件并将它导入到xcode的项目中,将下面代码添加到里面:

   
   
  1. var Coin = cc.Class.extend({
  2. space:null,
  3. sprite:null,
  4. shape:null,
  5. // 1.
  6. _map:0,
  7. get map() {
  8. return this._map;
  9. },
  10. set map(newMap) {
  11. this._map = newMap;
  12. },
  13. ctor:function (spriteSheet, space, pos) {
  14. this.space = space;
  15. // 2.
  16. var animFrames = [];
  17. for (var i = 0; i < 8; i++) {
  18. var str = "coin" + i + ".png";
  19. var frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
  20. animFrames.push(frame);
  21. }
  22. var animation = cc.Animation.create(animFrames, 0.1);
  23. var action = cc.RepeatForever.create(cc.Animate.create(animation));
  24. this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("coin0.png");
  25. // 3.
  26. var radius = 0.95 * this.sprite.getContentSize().width / 2;
  27. var body = new cp.StaticBody();
  28. body.setPos(pos);
  29. this.sprite.setBody(body);
  30. this.shape = new cp.CircleShape(body, radius, cp.vzero);
  31. this.shape.setCollisionType(SpriteTag.coin);
  32. // 4.
  33. this.shape.setSensor(true);
  34. this.space.addStaticShape(this.shape);
  35. // Needed for collision
  36. body.setUserData(this);
  37. // add sprite to sprite sheet
  38. this.sprite.runAction(action);
  39. spriteSheet.addChild(this.sprite, 1);
  40. },
  41. // 5.
  42. removeFromParent:function () {
  43. this.space.removeStaticShape(this.shape);
  44. this.shape = null;
  45. this.sprite.removeFromParent();
  46. this.sprite = null;
  47. },
  48. });
  49. // 6.
  50. var gCoinContentSize = null;
  51. Coin.getContentSize = function () {
  52. if (null == gCoinContentSize) {
  53. var sprite = cc.PhysicsSprite.createWithSpriteFrameName("coin0.png");
  54. gCoinContentSize = sprite.getContentSize();
  55. }
  56. return gCoinContentSize;
  57. };

同样提一些要点:

1. 金币属于哪个地图,这个值需要在ObjectManager.js中进行设置。

2. 初始化金币的动画

3. 金币采取使用静态的body方式来抵消重力

4. 传感器只是调用碰撞函数,并不会真的产生碰撞

5. 在ObjectManager.js中使用removeFromParent来删除元素

6. getContentSize是金币类中的一个静态方法,将在ObjectManager.js中用来计算坐标

接下来是石头,同样的方式创建一个Rock.js文件并添加下面的代码:

   
   
  1. var Rock = cc.Class.extend({
  2. space:null,
  3. sprite:null,
  4. shape:null,
  5. _map:0,// which map belong to
  6. get map() {
  7. return this._map;
  8. },
  9. set map(newMap) {
  10. this._map = newMap;
  11. },
  12. ctor:function (spriteSheet, space, pos) {
  13. this.space = space;
  14. // 1.
  15. if (pos.y >= (g_groundHight + Runner.getCrouchContentSize().height)) {
  16. this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("hathpace.png");
  17. } else {
  18. this.sprite = cc.PhysicsSprite.createWithSpriteFrameName("rock.png");
  19. }
  20. var body = new cp.StaticBody();
  21. body.setPos(pos);
  22. this.sprite.setBody(body);
  23. // 2.
  24. this.shape = new cp.BoxShape(body,
  25. this.sprite.getContentSize().width,
  26. this.sprite.getContentSize().height);
  27. this.shape.setCollisionType(SpriteTag.rock);
  28. this.shape.setSensor(true);
  29. this.space.addStaticShape(this.shape);
  30. spriteSheet.addChild(this.sprite);
  31. // Needed for collision
  32. body.setUserData(this);
  33. },
  34. removeFromParent:function () {
  35. this.space.removeStaticShape(this.shape);
  36. this.shape = null;
  37. this.sprite.removeFromParent();
  38. this.sprite = null;
  39. },
  40. });
  41. var gRockContentSize = null;
  42. Rock.getContentSize = function () {
  43. if (null == gRockContentSize) {
  44. var sprite = cc.PhysicsSprite.createWithSpriteFrameName("rock.png");
  45. gRockContentSize = sprite.getContentSize();
  46. }
  47. return gRockContentSize;
  48. };

岩石跟金币有以下两个不同点:

1. 石头有两个可根据Y­coordinate的值选择的纹理

2. 石头的形状不是circle,而是box

现在已经有了金币跟岩石,但是如何将它们添加到游戏中呢?

创建一个名为”ObjectManager.js”的文件并将它添加到Xcode项目中,用下面代码替换文件中的内容:

   
   
  1. require("Coin.js");
  2. require("Rock.js");
  3. var ObjectManager = cc.Class.extend({
  4. spriteSheet:null,
  5. space:null,
  6. // 1.
  7. objects:[],
  8. ctor:function (spriteSheet, space) {
  9. this.spriteSheet = spriteSheet;
  10. this.space = space;
  11. // objects will keep when new ObjectManager();
  12. // we need clean here
  13. this.objects = [];
  14. },
  15. // 2.
  16. initObjectOfMap:function (map, mapWidth) {
  17. var initCoinNum = 7;
  18. var jumpRockHeight = Runner.getCrouchContentSize().height + g_groundHight;
  19. var coinHeight = Coin.getContentSize().height + g_groundHight;
  20. // 2.1
  21. var randomCoinFactor = Math.round(Math.random()*2+1);
  22. var randomRockFactor = Math.round(Math.random()*2+1);
  23. var jumpRockFactor = 0;
  24. // 2.2
  25. var coinPoint_x = mapWidth/4 * randomCoinFactor+mapWidth*map;
  26. var RockPoint_x = mapWidth/4 * randomRockFactor+mapWidth*map;
  27. var coinWidth = Coin.getContentSize().width;
  28. var rockWith = Rock.getContentSize().width;
  29. var rockHeight = Rock.getContentSize().height;
  30. var startx = coinPoint_x - coinWidth/2*11;
  31. var xIncrement = coinWidth/2*3;
  32. //add a rock
  33. var rock = new Rock(this.spriteSheet, this.space,
  34. cc.p(RockPoint_x, g_groundHight+rockHeight/2));
  35. rock.map = map;
  36. this.objects.push(rock);
  37. if(map == 0 && randomCoinFactor==1){
  38. randomCoinFactor = 2;
  39. }
  40. //add 7 coins
  41. for(i = 0; i < initCoinNum; i++)
  42. {
  43. // 2.3
  44. if((startx + i*xIncrement > RockPoint_x-rockWith/2)
  45. &&(startx + i*xIncrement < RockPoint_x+rockWith/2))
  46. {
  47. var coin1 = new Coin(this.spriteSheet, this.space,
  48. cc.p(startx + i*xIncrement, coinHeight+rockHeight));
  49. } else{
  50. var coin1 = new Coin(this.spriteSheet, this.space,
  51. cc.p(startx + i*xIncrement, coinHeight));
  52. }
  53. coin1.map = map;
  54. this.objects.push(coin1);
  55. }
  56. for(i=1;i<4;i++){
  57. if(i!=randomCoinFactor&&i!=randomRockFactor){
  58. jumpRockFactor = i;
  59. }
  60. }
  61. // 2.4
  62. var JumpRockPoint_x = mapWidth/4 * jumpRockFactor+mapWidth*map;
  63. var jumpRock = new Rock(this.spriteSheet, this.space,
  64. cc.p(JumpRockPoint_x, jumpRockHeight+rockHeight/2));
  65. jumpRock.map = map;
  66. this.objects.push(jumpRock);
  67. },
  68. // 3.
  69. recycleObjectOfMap:function (map) {
  70. while((function (obj, map) {
  71. for (var i = 0; i < obj.length; i++) {
  72. if (obj[i].map == map) {
  73. obj[i].removeFromParent();
  74. obj.splice(i, 1);
  75. return true;
  76. }
  77. }
  78. return false;
  79. })(this.objects, map));
  80. },
  81. // 4.
  82. remove:function (obj) {
  83. obj.removeFromParent();
  84. // find and delete obj
  85. for (var i = 0; i < this.objects.length; i++) {
  86. if (this.objects[i] == obj) {
  87. this.objects.splice(i, 1);
  88. break;
  89. }
  90. }
  91. },
  92. });

一些需要注意的地方:

1. 所有的金币跟岩石都被放在一个列表中

2. 地图初始化对象的主要逻辑

  • 创建两个随机数来确定哪个点创建金币或岩石
  • 通过随机因素来计算每张地图中金币和岩石的 开始位置,将每个对象都添加到地图中
  • 用金币来举个例子,如果金币的开始位置跟岩石的一样那么就要把它的点调得比石头高度高或者低于石头的底部
  • 添加其他石头

    3. 每一次地图重载,地图中的对象要回收。

    4. 当游戏角色得到金币时,将这个金币从它的父类中和列表中移除

    一切都搞定了,现在唯一剩下的事就是PlayScene.js中进行整合,

    在PlayScene.js的前面添加下面的代码:

        
        
    1. require("ObjectManager.js");

    定义新的类成员变量

        
        
    1. objectManager:null,
    2. shapesToRemove:[],

    在init()函数中this.addChild(this.runner)这一行后面添加下面的代码:

        
        
    1. this.objectManager = new ObjectManager(this.spriteSheet, this.space);
    2. this.objectManager.initObjectOfMap(1, this.map.getMapWidth());

    在initPhysics()中设置chipmunk 的CollisionHandler

        
        
    1. this.space.addCollisionHandler(SpriteTag.runner, SpriteTag.coin,
    2. this.collisionCoinBegin.bind(this), null, null, null);
    3. this.space.addCollisionHandler(SpriteTag.runner, SpriteTag.rock,
    4. this.collisionRockBegin.bind(this), null, null, null);

    在PlayLayer中添加两个碰撞回调函数

        
        
    1. collisionCoinBegin:function (arbiter, space) {
    2. var shapes = arbiter.getShapes();
    3. this.shapesToRemove.push(shapes[1]);
    4. },
    5. collisionRockBegin:function (arbiter, space) {
    6. var rtn = this.runner.meetRock();
    7. if (rtn == true) {
    8. log("==gameover");
    9. director.pause();
    10. } else {
    11. // break Rock
    12. var shapes = arbiter.getShapes();
    13. this.shapesToRemove.push(shapes[1]);
    14. }
    15. },

    然后返回update()函数做如下更改:

        
        
    1. // Simulation cpSpaceAddPostStepCallback
    2. for(var i = 0; i < this.shapesToRemove.length; i++) {
    3. var shape = this.shapesToRemove[i];
    4. var body = shape.getBody();
    5. var obj = body.getUserData();
    6. //TODO add remove animation
    7. this.objectManager.remove(obj);
    8. }
    9. this.shapesToRemove = [];
    10. // check and reload map
    11. if (true == this.map.checkAndReload(this.lastEyeX)) {
    12. this.objectManager.recycleObjectOfMap(this.map.getCurMap() - 1);
    13. this.objectManager.initObjectOfMap(this.map.getCurMap() + 1, this.map.getMapWidth());
    14. //level up
    15. this.runner.levelUp();
    16. }

    现在,你已经完成了这个游戏的主要逻辑, 编译运行之,控制游戏角色去获取金币躲避岩石吧!骚年。

    你可以从这里获取全部的代码。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值