【新手入门教程】’用Cocos Creator给暗恋的女生写一个游戏

这是我写的一个系列博客,一共有15篇,由于霸屏有点不好,所以我都放在这一篇了
想单独看的可以传送到我的博客http://blog.csdn.net/potato47

【连载】用Cocos Creator给暗恋的女生写一个游戏(0)——本故事纯属虚构~~~~~

我们假设:

主人公叫做X,女主叫做思琪

他高中就认识了她

他高考考了594分,她考了605分

他在高考填志愿的那几天每天不停的问她,报了哪些学校,虽然他知道可能性不大,但他依然想跟她去同一所学校

她在填志愿的最后一天下午,听了老师的谣言,询问了自作聪明的校长,把第三个志愿也就是最可能去的学校改成了最垃圾的学校————江南大学

他不知道

录取通知下来,她被江南大学录取,他被南昌大学录取,江南大学最低录取分数线594分

大学的一半已经过去,他还没娶,她也未嫁

大二升大三的这个暑假,他想要给她写一个游戏,送给她当生日礼物

她的农历生日是八月初四,也就是开学后的九月四号,距离今天正好一个月

她追星,追了好多,金浩森,毕书尽,还有最近热播的老九门里的大佛爷————陈伟霆,投其所好,X决定做一个跟她的爱豆相关的游戏

假设完毕。

【连载】用Cocos Creator给暗恋的女生写一个游戏(1)——Cocos Creator初遇见

文字超限制了,这里省略吧。。。

【连载】用Cocos Creator给暗恋的女生写一个游戏(3)——游戏加载界面

恰好今天Creator1.2发布,我们紧跟潮流,就用1.2开始做吧

X给游戏起了一个名字47-21,其中深意自己体会。

(其实就是思琪名字的谐音,并且今年是她21岁的生日。。。当然你也可以理解为思琪爱你…)

因为X基本没有什么美术功底,所以一切简约为主。。。

先看一下要相处一个月的东西

X要做一个竖屏的游戏,所以他把布局改成了这个样子

并把偏好设置改成了竖屏

新建场景 Load,并在Canvas节点下新建单色Sprite,并给其添加widget组件,使其充满全屏,并调整背景颜色为天蓝色

在Bg节点下新建一个Layout节点,调整大小为530x530,添加组件widget,使其水平垂直居中

其中Resize Mode 选为Children,这种模式会固定布局的大小,调整子节点的大小,padding是子节点距离边界的距离,spacing是子节点之间的间距,将Sprite组件下的SpriteFrame图片删掉就可以去掉Layout的背景

新建两个Animation Clip取名为Change1和Change2,给数字96835添加Animation组件,以数字9为例,把Clip拖入属性面板就可以添加一个动画,然后点击动画编辑窗口的左上角,就可以开始编辑动画。

给动画添加opacity属性,在0:00和0:20添加两个关键帧,并在第二个关键帧里将节点的opacity改为0,动画播放速度调整为0.1,点击一下播放按钮就可以看见数字渐渐消失的效果,我们把动画保存,并给数字6 3 5都添加上刚才制作的动画。

现在开始做数字8的动画,同样的步骤将Change2添加节点数字8的default clip属性中,这里我们添加两个动画属性,一个是opacity,另一个是spriteFrame,第一个关键帧添加数字8图片,在第二个关键帧添加蛋糕图片,opacity改为0,其他属性如图

保存后点击运行预览,就会得到中间的几个数字消失的效果,然后蛋糕从中间出现,最终定格在下面的画面

添加进度条节点,调整大小位置如图,progress设置为0,将Bar节点的颜色调整为绿色

调整进度条大小时可以调整scale属性,子节点会随着父节点一起放大

新建脚本起名为“Load”,添加到Canvas节点上

Load.js

cc.Class({
    extends: cc.Component,

    properties: {
        loadBar: cc.ProgressBar,
    },

    // use this for initialization
    onLoad: function () {
        var load = function(){//十次过后加载下一场景
            this.loadBar.progress += 0.1;
            if(this.loadBar.progress > 0.9){
                cc.director.loadScene("Menu");
            }
        }
        this.schedule(load,0.8);
    },
});

这里我们启动了一个定时器,没0.8秒让进度条进一格,进度条满时进入菜单界面,因为我们的游戏本身比较简单,所以这里的加载界面只是一个假象,意思意思就行了。。。

最后要把进度条节点拖入到它的引用框

然后新建一个Menu场景,现在运行一下

【连载】用Cocos Creator给暗恋的女生写一个游戏(4)——游戏主菜单

既然是两个游戏,所以X想用两个主页面,最理想的就是用pageview,可是Creator还没支持,所以用一个差不多的模拟一下——ScrollView

新建一个ScrollView空节点,添加ScrollView组件,使其大小充满全屏,在content节点下添加两个单色的Sprite节点,分别取名为PageLeft和PageRight调整颜色大小如图

再改造一下变成下面的样子

给两个按钮分别添加点击事件(新建Menu脚本挂载在Canvas节点上)

Menu.js

cc.Class({
    extends: cc.Component,

    properties: {

    },

    // use this for initialization
    onLoad: function () {

    },
    
    onBtnPlay1: function(){
        cc.director.loadScene("RunGame");
    },
    
    onBtnPlay2: function(){
        cc.director.loadScene("JumpGame");
    },

});

现在非常简约风格的主菜单界面就做好了,但是太单调了,我们再添加一个小动画

我们在屏幕的左下角添加一个Sprite节点显示我们的主角,然后编辑一个自拍的动画

我们再制作以一个气泡对话的效果

这就要讲到游戏素材的经典部分了,注意听讲(敲黑板)

我们把准备好的气泡图片双击打开,可以看到有四条绿色的线可以移动,把它们移动到如下图的位置,标横线的部分可以横向伸缩,竖线部分可以纵向伸缩,中间的十字部分横纵都伸缩,而四个角画叉的部分不会收伸缩影响,制作完的气泡图片不论怎么放大,四个角的圆角以及左边的小尖就不会变形了(请那位说“早就知道了”的同学出去)

点击右上角的对号保存,然后把制作好的图片添加到Tips节点下,修改图片模式为sliced(九宫格),然后调整图片的size属性得到如图效果(一定要改size,不要改scale,scale不会考虑图片模式),还要记得挂载widge组件让Tips节点保持在屏幕的底边

修改Menu脚本

cc.Class({
    extends: cc.Component,

    properties: {
        
        tipsLabel:cc.Label,
        
        tipsText:{
            type:cc.String,
            default:[],

        }
    },

    onLoad: function () {
        let self = this;
        var show = function(){
            if(self.tipsText.length>0){
                var n = Math.round(Math.random()*(self.tipsText.length-1));//随机选择tipsText数组中的一个
                self.tipsLabel.string = self.tipsText[n];
            }
        }
        show();
        this.schedule(show,10);//每十秒更新一次Tips
    },
    
    onBtnPlay1: function(){
        cc.director.loadScene("RunGame");
    },
    
    onBtnPlay2: function(){
        cc.director.loadScene("JumpGame");
    },

});

在Tips节点下添加一个Label节点,并拖入Menu脚本,在属性面板可以任意添加Tips的文本,但尽量不要太长。。。

看一下效果

【连载】用Cocos Creator给暗恋的女生写一个游戏(5)——(JumpGame)场景搭建

先看图

这个场景我们只用了两张小图片素材,分别是A1和B1对应的图片,而A2和B2是复制前面的,可以看出,A和B部分明显长出屏幕的宽,这是为了实现背景无限移动的效果(当图片滚动到一定距离,我们让它再重新滚动。这样就可以简单的实现无限背景)。

复习时间到:我们的原始图片素材并没有这么高,所以要让它纵向拉伸,问题来了,怎么才能让纵向拉伸的图片不变形呢?

请在心里默数三个数,再看答案

3

2

1

再添加几个元素

看到下面那个能量槽了么,是不是有点像天天酷跑了?(请说“一点也不像”和“没玩过天天酷跑”的那两位同学出去)

接下来就要添加水管了

因为水管是游戏中不断重复出现的,所以我们把它做成prefab

什么是prefab呢?就是游戏中需要重复出现的东西。。。

任意位置创建节点PipeGroup,并给其添加两个子节点PipeTop和PipeBot,改变大小位置如图

其中要更改上下两个水管的锚点,使其在管口的位置,下面的管子更改属性scaleY为-1即可使管子倒过来,最后将制作好的节点拖入资源管理器形成prefab,这样当需要用到时就可以“复制”一个出来

但好像有一个问题,这个水管兄弟好像太长了,上面的还好说,因为在屏幕外的看不见,但下面的水管和地面的显示效果不对,我们把它藏在地面后面

在天空和地面节点中间再添加一个空节点,取名为PipeLayer,不用设置大小和位置,他只是一个标记显示层次的节点,现在把水管兄弟添加到PipeLayer节点下,就能实现我们想要的效果了

最后在加上我们主角的大老公(学PS技术请上【此处招商】)

初步设定,吃到一个老公加一个能量,集齐十个老公可以召唤神龙

最后的最后,我们添加一个结束菜单

把右上角的勾去掉,结束菜单当然要在游戏结束才能显示,我们通过node.active = ture or false来控制它是否显示

最后的最后的最后,给主角添加两个动画

【连载】用Cocos Creator给暗恋的女生写一个游戏(6)——(Run Game)主角逻辑

我们在Canvas节点的同级添加一个Game节点,并添加两个子节点如图

新建一个脚本取名叫做“Game”并添加到Game节点上

Game.js

var Player = require("Player");
cc.Class({
    extends: cc.Component,

    properties: {
        player:Player,
    },

    onLoad: function () {
        //返回键返回菜单
        cc.eventManager.addListener({
            event: cc.EventListener.KEYBOARD,
            onKeyPressed: function(keyCode, event) {
                if(keyCode == cc.KEY.back){
                    cc.director.loadScene('Menu');
                }
            }
        }, this.node);
        this.startGame();
    },
    
    startGame: function(){
        this.player.init(this);
    },
    
    stopGame: function(){
        
    },
    
});

作为一个专业的游戏开发者(一脸严肃,咳咳),我们有必要探讨一下游戏的设计模式

我们知道一个游戏中会有很多元素,对应到我们设计的很多节点,按理说每个节点都有自己的逻辑,有时多个节点彼此也有联系,我们把每个节点想象成一个人,如果每个人都乱交(胡乱交际的意思,请那位早熟的同学出去),那这个游戏的世界就乱套,所以我们需要一位领导者来领导这些人,每一位普通人都各司其职,当他需要其他人的帮助时,要向领导者请示,这样一来,所有普通人都只与领导者联系,领导者控制者所有普通人,世界一片美好。。。我们把这个模式叫做领导者模式(这个名字是我自己起的。。。。)

这个Game就相当于领导者,我们在其中写游戏的主体逻辑和管理各个节点的重要方法,像开始游戏,结束游戏和各个节点的初始化之类

require方法可以得到脚本组件,我们在属性里添加一个player让它的类型是require(“Player”),然后在将Player节点拖入对应位置,player属性就会引用Player节点的Player脚本,这样就可以在Game.js里调用Player.js的方法了(Player.js应先建立,不然require不到文件会报错)

新建一个脚本取名叫做“Player”并添加到Player节点上

Player.js

var STATE = cc.Enum({
    NONE:0,
    NORMAL:1,
    SUPER:2,
    DEAD:3,
});
cc.Class({
    extends: cc.Component,

    properties: {
        speedX:0,
        gravity:0,
        jumpSpeed:0,
        groundY:0,
        state:{
            default:STATE.NONE,
            type:STATE,
            visible:false,//属性面板不显示
        }
    },

    // // use this for initialization
    init: function (game) {
        this.game = game;
        this.speedY = 0;
        this.state = STATE.NORMAL;
        this.registerInput();
    },
    
    registerInput: function(){
        let self = this;
        //键盘事件
        cc.eventManager.addListener({
            event: cc.EventListener.KEYBOARD,
            onKeyPressed: function(keyCode, event) {
                if(keyCode == cc.KEY.back){
                    cc.director.loadScene("Menu");
                }else{
                    self.jump();
                }
            }
        }, self.node);
        //触摸事件
        cc.eventManager.addListener({
            event: cc.EventListener.TOUCH_ONE_BY_ONE,
            onTouchBegan: function(touch, event) {
                self.jump();
            }
        }, self.node);
    },
    
    jump:function(){
        this.speedY = this.jumpSpeed;  
    },
    
    strengthen:function(){
        this.state = STATE.SUPER;
    },
    
    recover:function(){
        this.state = STATE.NORMAL;
    },
    
    die:function(){
        this.state = STATE.DEAD;  
    },

    update: function (dt) {
        if(this.state != STATE.NONE && this.state != STATE.DEAD){
            this.speedY -= this.gravity * dt;
            this.node.y += this.speedY * dt;
            if(this.node.y <= this.groundY){
                this.node.y = this.groundY;
            }
        }
    },
});

我们从上往下看

大多数情况下我们都会给主角划分状态,我这里给主角设定了四个状态

NONE:游戏开始前,什么状态都没有

NORMAL:正常状态

SUPER:超级赛亚人形态。。。

DEAD:死

主角的一些基本属性

speedX:主角的横向速度,因为相对屏幕水平是不移动的,这个x轴速度是用来移动背景的,暂时不用管,

gravity:重力,

jumpSpeed:起跳速度,

groundY:地面位置,我们让它等于主角落在地面是主角的Y轴坐标,主角是不能掉到地面下方的,我们会在更新主角位置时判断

state:主角的状态,一开始我们把它设为无状态(OMG啊。。。)

当领导发话“开始游戏了!”,player就执行init方法,初始化一些乱七八糟的东西,让主角状态变成normal,注册事件

中间的几个改变主角状态的方法,会在特定的时候调用

最下面的update会根据主角的属性更新主角的位置

看一下效果

【连载】用Cocos Creator给暗恋的女生写一个游戏(7)——(Run Game)背景移动

新建CameraManager.js添加到在Game的子节点CameraManager下

CameraManager.js

cc.Class({
    extends: cc.Component,

    properties: {
        far:cc.Node,
        farRelSpeed:0,//相对主角移动速度
        farOffX:0,//循环滚动距离
        ground:cc.Node,
        groundRelSpeed:0,
        groundOffX:0,
        pipeLayer:cc.Node,
        layerRelSpeed:0,
    },

    init: function (game) {
        this.game = game;
        this.oFarX = this.far.x;
        this.oGroundX = this.ground.x;
    },

    moveBg: function(distance){
        this.far.x -= distance * this.farRelSpeed;
        if(this.far.x < (this.oFarX - this.farOffX)){
            this.far.x = this.oFarX;
        }
        this.ground.x -= distance * this.groundRelSpeed;
        if(this.ground.x < (this.oGroundX-this.groundOffX)){
            this.ground.x = this.oGroundX;
        }
        this.pipeLayer.x -= distance * this.layerRelSpeed;
    },
});

修改Game.js

var Player = require("Player");
var CameraManager = require("CameraManager");
cc.Class({
    extends: cc.Component,

    properties: {
        player:Player,
        cameraManager:CameraManager,
    },

    onLoad: function () {
        //返回键返回菜单
        cc.eventManager.addListener({
            event: cc.EventListener.KEYBOARD,
            onKeyPressed: function(keyCode, event) {
                if(keyCode == cc.KEY.back){
                    cc.director.loadScene('Menu');
                }
            }
        }, this.node);
        this.startGame();
    },
    
    startGame: function(){
        this.cameraManager.init(this);
        this.player.init(this);
    },
    
    stopGame: function(){
        
    },
    
});

修改Player.js

...
update: function (dt) {
    if(this.state != STATE.NONE && this.state != STATE.DEAD){
        this.speedY -= this.gravity * dt;
        this.node.y += this.speedY * dt;
        if(this.node.y <= this.groundY){
            this.node.y = this.groundY;
        }
        this.game.cameraManager.moveBg(this.speedX * dt);
    }
},
...

下面我要用我超强的逻辑思维给大家解释一下这个背景是怎么移动的

咳咳

我们知道每一次调用update方法,player的位置就会根据相应属性更新,而背景的移动应该跟player的移动相关,(这里我们可能要讲一下爱因斯坦的相对论,但可能讲完就下课了,所以不讲了…)我们需要知道player往前移动,背景就要往后移动,它们的速度是相反的。

还有远景和近景的移动速度不同,远处的移动速度要慢一些(那位举“坐火车”的例子的同学很聪明)

所以我们在CameraManager里设置了far相对player的速度 farRelSpeed 和ground相对于player的速度 groundRelSpeed 以及 layerRelSpeed ,因为ground,pipeLayer和player是在同一平面的,所以他们的相对速度都设为1,far节点较远,设为0.2

还有两个属性farOffX和groundOffX,他们是记录两个节点循环滚动距离的,因为我们的背景只有两张图片,需要循环显示,那个farOffX = 1000是计算出来的,留给同学们当作业,做完明天班长收一下

最后我们要把上次没用到的Player的speedX填上数值

通过这么巧妙的设置,我们就可以随便更改主角的速度而不用管背景的移动速度了,因为背景会随着主角速度动态调整

看一下效果

【连载】用Cocos Creator给暗恋的女生写一个游戏(8)——(Run Game) Prefab的动态管理#

我们先来考虑一下这个PrefabManager是干什么的,我们的游戏中有重复出现的水管和老公,所以PrefabManager要能够动态管理他们,理所当然要有创建和销毁的方法,创建很简单,我们只需要每隔一段距离往pipeLayer添加子节点就行了,但销毁它们还是需要一些技巧的,从逻辑角度来说,当水管和老公移动到屏幕外时我们就要将他们销毁,这里我们用一个普通班不会教的方法——利用碰撞检测系统

我们先给PrefabManager添加一个碰撞组件,编辑如图

给水管兄弟添加碰撞组件,设置tag为333

给水管哥哥添加碰撞组件,设置tag为3331

给水管弟弟添加碰撞组件,设置tag也为3331

给老公添加碰撞组件,设置tag为666

PrefabManager.js

cc.Class({
    extends: cc.Component,

    properties: {
        pipeGroupPre:cc.Prefab,
        starPre:cc.Prefab,
        pipeLayer:cc.Node,
        //上下管子之间距离范围
        spacingRange: cc.p(0,0),
        // 下面管子Y轴偏移量范围
        botYRange: cc.p(0,0),
        // 左右管子之间距离
        pipeSpace:0,
        //第一个管子位置
        oPipeX:0,
        //星星位置范围
        starYRange:cc.p(0,0),
    },

    init: function (game) {
        this.game = game;
        this.pipePool = new cc.NodePool();
        this.starPool = new cc.NodePool();
        for(var i=0;i<4;i++){
            this.pipePool.put(cc.instantiate(this.pipeGroupPre));
            this.starPool.put(cc.instantiate(this.starPre));
        }
        this.curPipeX = this.oPipeX;//当前管子位置
        this.spawnPipe();
        this.spawnPipe();
        
        cc.director.getCollisionManager().enabled = true;
    },

    onCollisionEnter: function(other,self){
        if(other.tag === 333){
            this.desPipe(other.node);
        }else if(other.tag === 666){
            this.desStar(other.node);
        }
    },

    spawnPipe: function(){
        var pipeGroup = this.pipePool.get();
        var pipeTop = pipeGroup.getChildByName("PipeTop");
        var pipeBot = pipeGroup.getChildByName("PipeBot");
        var botYPos = this.botYRange.x + Math.random() * (this.botYRange.y - this.botYRange.x);
        var space = this.spacingRange.x + Math.random() * (this.spacingRange.y - this.spacingRange.x);
        var topYPos = botYPos + space;
        pipeTop.y = topYPos;
        pipeBot.y = botYPos;
        pipeGroup.x = this.curPipeX;
        this.pipeLayer.addChild(pipeGroup);
        
        this.spawnStar();
        
        this.curPipeX += this.pipeSpace;
    },
    
    desPipe: function(node){
        this.pipePool.put(node);
        this.spawnPipe();
    },
    
    spawnStar: function(){
        if(Math.random() < 0.8){
            var star = this.starPool.get();
            star.y = this.starYRange.x + Math.random()*(this.starYRange.y - this.starYRange.x);
            star.x = this.curPipeX + this.pipeSpace/2;
            this.pipeLayer.addChild(star);
        }
    },
    
    desStar: function(node){
        this.starPool.put(node);
    },

});

按照惯例,我们从上往下看

在属性中我们定义了需要用到的两个Prefab和一个pipeLayer节点,还有一些随机设置Prefab的参数(这里用二维的点来表示范围,x是最小值,y是最大值)

第一个管子的位置要在屏幕外

在init方法里我们定义了两个cc.NodePool,分别存放pipeGroup和star

最后排的那位同学问了一个问题"为什么要用NodePool,直接用instantiate复制和destroy销毁不就行了么?"

老师很表扬这种勇于提问的表现,但也要批评这位同学不好好预习的行为

现在我们来翻一下官方文档

“记住了吗”

“记住了”

屏幕里最多出现两个水管,因为人生难免有意外,所以我们在pool里放四个,

当prefab滚到屏幕外的回收区时,我们就把它放回pool里,这样就可以实现一个良性循环,毕竟全球变暖了,话说今年好像比去年热了,话说我好像跑题了。。。

每添加一个pipeGroup我们就要调用一次spawnStar方法,但老公不是一个随便的人,所以我们要给他添加一个出现的概率,0.8吧,毕竟老公是我们这个游戏的唯一吸引力。。。

最后我们要在Game.js里初始化PrefabManager

var Player = require("Player");
var CameraManager = require("CameraManager");
var PrefabManager = require("PrefabManager");
cc.Class({
    extends: cc.Component,

    properties: {
        player:Player,
        cameraManager:CameraManager,
        prefabManager:PrefabManager,
    },

    onLoad: function () {
        //返回键返回菜单
        cc.eventManager.addListener({
            event: cc.EventListener.KEYBOARD,
            onKeyPressed: function(keyCode, event) {
                if(keyCode == cc.KEY.back){
                    cc.director.loadScene('Menu');
                }
            }
        }, this.node);
        this.startGame();
    },
    
    startGame: function(){
        this.cameraManager.init(this);
        this.prefabManager.init(this);
        this.player.init(this);
    },
    
    stopGame: function(){
        
    },
    
    gainScore: function(){
        
    },
    
    gainEnergy:function(){
        
    },
});

看一下效果

【连载】用Cocos Creator给暗恋的女生写一个游戏(9)——(Run Game) 游戏逻辑与碰撞检测#

至此为止,我们已经把主要的技术问题都解决了,现在我们给游戏添加
具体的逻辑

给主角添加碰撞组件

回顾一下我们已经添加的碰撞组件的节点

管子兄弟:tag=333

管子哥哥和管子弟弟:tag=3331

大老公:tag=666

主角可以跟它们产生碰撞,碰撞的效果如下

管子兄弟:(离开时)加分(相当于已经跳过了一组管子)

管子哥哥或者管子弟弟:主角死亡,游戏失败

大老公:能量槽加1,大老公销毁

完善主角逻辑,重新修改如下

Player.js

var STATE = cc.Enum({
NONE:0,
NORMAL:1,
SUPER:2,
DEAD:3,
});
cc.Class({
    extends: cc.Component,

    properties: {
        oSpeedX:0,
        superSpeedX:0,
        gravity:0,
        jumpSpeed:0,
        groundY:0,
        state:{
            default:STATE.NONE,
            type:STATE,
            visible:false,//属性面板不显示
        }
    },

    init: function (game) {
        this.game = game;
        this.speedY = 0;
        this.speedX = this.oSpeedX;
        this.state = STATE.NORMAL;
        this.registerInput();
        //cc.director.getCollisionManager().enabled = true;
    },
    
    onCollisionEnter:function(other,self){
        if(this.state == STATE.NORMAL){
            if(other.tag == 666){
                this.game.gainEnergy();
                this.game.prefabManager.desStar(other.node);
            }
            if(other.tag == 3331){
                this.die();
            }
        }
    },
    
    onCollisionExit:function(other,self){
        if(other.tag == 333){
            this.game.gainScore();
        }
    },
    
    registerInput: function(){
        let self = this;
        //键盘事件
        cc.eventManager.addListener({
            event: cc.EventListener.KEYBOARD,
            onKeyPressed: function(keyCode, event) {
                if(keyCode == cc.KEY.back){
                    cc.director.loadScene("Menu");
                }else{
                    self.jump();
                }
            }
        }, self.node);
        //触摸事件
        cc.eventManager.addListener({
            event: cc.EventListener.TOUCH_ONE_BY_ONE,
            onTouchBegan: function(touch, event) {
                self.jump();
            }
        }, self.node);
    },
    
    jump:function(){
        this.speedY = this.jumpSpeed;  
    },
    
    strengthen:function(){
        this.state = STATE.SUPER;
        this.node.color = cc.Color.RED;
        this.speedX = this.superSpeedX;
        //5秒超级速度,2秒普通速度进行缓冲,一共7秒无敌
        let self = this;
        var cache = function(){
            self.speedX = self.oSpeedX;
        }
        this.scheduleOnce(cache,5);
        this.scheduleOnce(this.recover,7);
    },
    
    recover:function(){
        this.state = STATE.NORMAL;
        this.node.color = cc.Color.WHITE;
        this.oSpeedX += 10;//给游戏加一点难度
        this.speedX = this.oSpeedX;
        this.game.energyBar.progress = 0;
        this.game.energyBar.node.getChildByName("Bar").color = cc.Color.GREEN;
    },
    
    die:function(){
        this.state = STATE.DEAD;  
        this.node.color = cc.Color.BLACK;
        this.game.stopGame();
    },

    update: function (dt) {
        if(this.state != STATE.NONE && this.state != STATE.DEAD){
            this.speedY -= this.gravity * dt;
            this.node.y += this.speedY * dt;
            if(this.node.y <= this.groundY){
                this.node.y = this.groundY;
            }
            this.game.cameraManager.moveBg(this.speedX * dt);
        }
    },
});

最终效果

结束菜单

至此,我们的第一个小游戏就结束了

【连载】用Cocos Creator给暗恋的女生写一个游戏(10)——(Jump Game) 场景搭建等#

我先来说说,第二个游戏是个什么样子的

我们继续用第一个游戏的主角,这里的场景是天空,天空中有很多云,有的云很老实,有的云很脆弱,有的云很好动,我们的主角目的只有一个——上天

经过我的无数次实验(其实也没有很多次啦。。),设定了主角的移动方式为自动跳跃,玩家控制左右方向,水平速度一直存在,这样可以给游戏增加一点难度,玩家需要一直左右调整方向

为了上天的路上不太单调,我们在天上随机放一些老公,吃到老公加能量,能量槽满,主角开启加速模式。

是不是似曾相识?

当你做两个很像的游戏时,你会发现你掌握了很多东西

所以这节课,同学们上自习吧,我把题目发给大家

【连载】用Cocos Creator给暗恋的女生写一个游戏(10)——(Jump Game) 镜头跟随#

上节自习大家是不是上的很开心啊。。。。

大家是不是发现了两个游戏有辣么多相同的地方,就连很多节点的名字都一样

但是有一个东西是第一游戏没有的,那就是中间的那个Camera

我把Camera节点添加上颜色

Camera就是标记一块区域,这块区域是干什么的呢?

又是最后面那位宝强同学说出了正确答案

这块区域就是镜头范围

拿拍电影来举例子,当拍一个场景时,场景里常常有主角,而主角常常有主角光环,所以摄像机的镜头就要对准主角,镜头要跟着主角移动

这里的镜头移动方式有两种

一种是中心移动,也就是以主角为中心,主角移动镜头就同步移动

另一种是边界移动,这种移动方式下我们给镜头设定一个范围,当主角移动出这个范围时,摄像机才移动

这在游戏中也很常见,比如英雄联盟的视角锁定,为了更贴切的感受,强烈建议你打开游戏撸一局,自由视角和中心视角的切换按钮是Y(老师是鼓励你们上网,早恋的,不然等你们到大学就晚了)

我们这里用的是边界移动的方式(中心移动的我也试了一下,真的有点晕)

CameraManager2.js

cc.Class({
    extends: cc.Component,

    properties: {
        bgSky:cc.Node,
        skySca:0,
        bgHill:cc.Node,
        hillSca:0,
        bgHillnear:cc.Node,
        hillnearSca:0,
        bgFloor:cc.Node,
        floorSca:0,
        cloudLayer:cc.Node,
        cloudSca:0,
        camera: cc.Node,
        player: cc.Node,
        
    },
    
    init: function(game){
        this.game = game;
    },

    moveBg: function(distance){
		//当主角跳出镜头边界时镜头才移动
        if(this.player.y > this.camera.y+this.camera.height/2){
            this.bgSky.y -= distance * this.skySca;
            this.bgHill.y -= distance * this.hillSca;
            this.bgHillnear.y -= distance * this.hillnearSca;
            this.bgFloor.y -= distance * this.floorSca;
            this.cloudLayer.y -= distance * this.cloudSca;
            this.player.y -= distance;
        }
    }
});

看看效果

【连载】用Cocos Creator给暗恋的女生写一个游戏(12)——跨场景访问节点、存储数据、添加音效音乐、打包发布#

跨场景访问节点

我们来看一下之前做的菜单场景

中间有一个记录的label我们一直没理她,

今天我们就来翻她的牌子

我们每次游戏结束时都会有一个分数,这个分数变量在相应的游戏场景里,我们想要的效果时:当返回菜单时,我们要把这个分数变量带回来,但当场景销毁时,其中的所有节点都会随之消失

这里就要引出另一个重要的知识点,同学们拿笔记一下

没带笔的前后桌借一下

不会跟随场景销毁的节点——常驻节点

我们回到第一个Load场景,添加一个根节点(必须是根节点哦)Record

Record.js

cc.Class({
    extends: cc.Component,

    properties: {
        bestRunScore: 0,
        bestJumpScore: 0,
    },

    onLoad: function () {
        cc.game.addPersistRootNode(this.node);
        var bestRunScore = cc.sys.localStorage.getItem("bestRunScore");
        if(bestRunScore){
            this.bestRunScore = bestRunScore;
        }
        var bestJumpScore = cc.sys.localStorage.getItem("bestJumpScore");
        if(bestRunScore){
            this.bestJumpScore = bestJumpScore;
        }
    },

    updateRunScore: function(score){
        if(score > this.bestRunScore){
            this.bestRunScore = score;
        }
    },
    
    updateJumpScore: function(score){
        if(score > this.bestJumpScore){
            this.bestJumpScore = score;
        }
    },

    save(){
        cc.sys.localStorage.setItem('bestRunScore', this.bestRunScore);
        cc.sys.localStorage.setItem('bestJumpScore', this.bestJumpScore);
    },
});

我们在onLoad方法里将Record节点变成游戏的常驻节点

cc.game.addPersistRootNode(this.node);

销毁的方法是

cc.game.removePersistRootNode(node);

游戏中的常驻节点,在切换场景时不会销毁,所以我们可以把一些需要跨场景访问的方法和变量添加到常驻节点的脚本里

我们切换到Menu场景

宝强同学惊奇的发现,那个常驻节点Record并没有出现!

宝强同学就是太实在

有些事情光靠眼睛是发现不了的

常驻节点只是逻辑上的,并不会在其他场景层级管理器里出现

“层级管理器里都没有,那怎么用啊”

这就要用另一个找节点的方法了

cc.find();

我们修改两个Game的stopGame方法

Game.js

stopGame: function(){
    cc.director.getCollisionManager().enabled = false;
    this.gameOverMenu.active = true;
    this.overScore.string = this.score+"m";
	//存储数据
    cc.find("Record").getComponent("Record").updateRunScore(this.score);
},

Game2.js

stopGame: function(){
	cc.director.getCollisionManager().enabled = false;
    this.gameOverMenu.getChildByName('OverScore').getComponent(cc.Label).string = this.score;
    this.gameOverMenu.active = true;
    //存储数据
    cc.find("Record").getComponent("Record").updateJumpScore(this.score);
},

存储与读取数据

存储数据

cc.sys.localStorage.setItem('bestRunScore', this.bestRunScore);
cc.sys.localStorage.setItem('bestJumpScore', this.bestJumpScore);

读取数据

cc.sys.localStorage.getItem("bestRunScore");
cc.sys.localStorage.getItem("bestJumpScore");

就是两个封装的方法,存储的方式是键值对

Menu脚本也需要修改一下

Menu.js

...
onLoad: function () {
    this.record = cc.find("Record").getComponent("Record");
    this.runScore.string = "你最远跑了" + this.record.bestRunScore+ "m";
    this.jumpScore.string = "你最高跳了" + this.record.bestJumpScore+ "m";
	...
},
...

播放声音

播放声音有两种方式

1.组件AudioSource

play ( )
播放音频剪辑。

stop ( )
停止当前音频剪辑。

pause ( )
暂停当前音频剪辑。

resume ( )
恢复播放。

rewind ( )
从头开始播放。

2.声音系统

cc.audioEngine.playMusic(source);
cc.audioEngine.stopMusic(source);
cc.audioEngine.playEffect(source);
cc.audioEngine.stopEffect(source);

上面的第一种方法原生平台有很多Bug,所以我们的游戏都用的第二种方法播放声音

打包发布(修改图标)

我们打包一个Android的Apk

模板选择binary可以快速打包,Portrait竖屏,Upside Down倒屏,Landscape Left左右横屏,Landscape Right右左横屏

我们先点击构建

构建完成后我们找到构建的目录(就是上面的build文件夹)

build/jsb-binary/frameworks/runtime-src/proj.android/res/

将里面的图标换成我们的女主角

替换完成后再点击编译

进入下面的目录就可以看见我们的Apk文件了

【连载】用Cocos Creator给暗恋的女生写一个游戏(13)——整体回顾,工程文件#

Load场景

Menu场景

RunGame场景

JumpGame场景

(省略了原文的一些代码,和最后一篇随便写的东西)

打包apk文件

资源工程文件

18赞

都这么污了,还有什么话好说呢

1赞

好给力的教程!

这别具一格的讲解风格。。。

灵魂玩家!

对不起,我笑了

对不起,我笑了

对不起,我也笑了

谢谢楼主无私奉献,来看看镜头跟随

不是特别懂,但你暗恋的女神是1947年2月1日生的吗

楼主让我转告各位,不要再顶这个帖子了

楼主好可爱 啊

赞! mark

对不起,我笑了

520快到了 那女生还在吗

男人得有志气,Creator在手,天下我有,的确有亿分之一的机会写出MineCraft的,那时各国当家维多利亚的秘密女孩还不是都会乖乖和你约会,超越C罗之流。

穷不怕,就怕没有志气。

1赞

楼主让我再次转告各位,不要再顶这个帖子了

:grinning:想知道楼主的后续

顶 

我能再顶一次吗,毕竟是个好教程 :face_with_hand_over_mouth: :face_with_hand_over_mouth: