Java – byte[] 和 String互相转换

通过用例学习Java中的byte数组和String互相转换,这种转换可能在很多情况需要,比如IO操作,生成加密hash码等等。

除非觉得必要,否则不要将它们互相转换,他们分别代表了不同的数据,专门服务于不同的目的,通常String代表文本字符串,byte数组针对二进制数据

通过String类将String转换成byte[]或者byte[]转换成String

用String.getBytes()方法将字符串转换为byte数组,通过String构造函数将byte数组转换成String

注意:这种方式使用平台默认字符集

复制代码
复制代码
package com.bill.example;
 
public class StringByteArrayExamples 
{
    public static void main(String[] args) 
    {
        //Original String
        String string = "hello world";
         
        //Convert to byte[]
        byte[] bytes = string.getBytes();
         
        //Convert back to String
        String s = new String(bytes);
         
        //Check converted string against original String
        System.out.println("Decoded String : " + s);
    }
}
复制代码
复制代码

输出:

hello world

通过Base64 将String转换成byte[]或者byte[]转换成String[Java 8]

可能你已经了解 Base64 是一种将二进制数据编码的方式,正如UTF-8和UTF-16是将文本数据编码的方式一样,所以如果你需要将二进制数据编码为文本数据,那么Base64可以实现这样的需求

从Java 8 开始可以使用Base64这个类

复制代码
复制代码
import java.util.Base64;
public class StringByteArrayExamples 
{
    public static void main(String[] args) 
    {
        //Original byte[]
        byte[] bytes = "hello world".getBytes();
         
        //Base64 Encoded
        String encoded = Base64.getEncoder().encodeToString(bytes);
         
        //Base64 Decoded
        byte[] decoded = Base64.getDecoder().decode(encoded);
         
        //Verify original content
        System.out.println( new String(decoded) );
    }
}
复制代码
复制代码

输出:

hello world

总结

在byte[]和String互相转换的时候你应该注意输入数据的类型

  1. 当使用String类的时候,将String作为输入类型
  2. 当使用Base64类的时候,使用byte数组作为输入类型

如有问题请在评论留言

Happy Learning !!

本文转自:https://www.cnblogs.com/keeplearnning/p/7003415.html

利用java socket和sampled实现点对点即时语音通信

利用java socket和sampled实现点对点即时语音通信

基本思路
​ 利用javax.sound.sampled 包中的方法可以很方便的实现,获取拾音器音频输入的内容,和写入音频输出的混频器中。结合socket可以实现点对点语音通话。

需要用到的类
AudioFormat类
​ 是在声音流中指定特定数据安排的类。通过检查以音频格式存储的信息,可以发现在二进制声音数据中解释位的方式。每个数据行都有与其数据流相关的音频格式。源(回放)数据行的音频格式指示数据行期望接收输出的数据类型。对于目标(捕获)数据行,音频格式指定可以从该行读取的数据种类。当然,声音文件也有音频格式。AudioFileFormat

类封装 AudioFormat 以及其他特定于文件的信息

TargetDataLine类
​ 目标数据行是可以从中读取音频数据的某种类型的 DataLine。最常见的示例是从音频捕获设备获取其数据的数据行。(该设备被实现为写入目标数据行的混频器。)

SourceDataLine类
​ 源数据行是可以写入数据的数据行。它充当其混频器的源。应用程序将音频字节写入源数据行,这样可处理字节缓冲并将它们传递给混频器。混频器可以将这些样本与取自其他源的样本混合起来,然后将该混合物传递到输出端口之类的目标(它可表示声卡上的音频输出设备)

AudioSystem类
AudioSystem
类充当取样音频系统资源的入口点。此类允许查询和访问安装在系统上的混频器。AudioSystem
包括许多在不同格式间转换音频数据的方法,以及在音频文件和流之间进行转换的方法。它还提供不用显式处理混频器即可直接从
AudioSystem 获得 Line 的方法

TargetDataLine td = (TargetDataLine)(AudioSystem.getLine(info));

Obtains a line that matches the description in the specified Line.Info object.

If a DataLine is requested, and info is an instance of DataLine.Info specifying at least one fully qualified audio format, the last one will be used as the default format of the returned DataLine.

If system properties javax.sound.sampled.Clip, javax.sound.sampled.Port, javax.sound.sampled.SourceDataLine and javax.sound.sampled.TargetDataLine are defined or they are defined in the file “sound.properties”, they are used to retrieve default lines. For details, refer to the class description. If the respective property is not set, or the mixer requested in the property is not installed or does not provide the requested line, all installed mixers ar e queried for the requested line type. A Line will be returned from the first mixer providing the requested line type.

具体实现
1、获取目标数据行,这里是从音频捕获设备(拾音器)获取其数据的数据行

​ 在本应用中从音频捕获设备(拾音器)获取其数据的数据行,会发送给对方作为对方的音频源数据。

// 1.获取音频流数据
// af为AudioFormat也就是音频格式
AudioFormat af = getAudioFormat();
DataLine.Info info = new DataLine.Info(TargetDataLine.class, af);
// 这里的td实际上是
TargetDataLine targetDataLine = (TargetDataLine) (AudioSystem.getLine(info));
// 打开具有指定格式的行,这样可使行获得所有所需的系统资源并变得可操作。
targetDataLine.open(af);
// 允许某一数据行执行数据 I/O
targetDataLine.start();

//将数据读取到bts中
int len = targetDataLine.read(bts, 0, bts.length);

2、获取源数据行 ,将音频字节写入源数据行,这样可处理字节缓冲并将它们传递给混频器。混频器可以将这些样本与取自其他源的样本混合起来,然后将该混合物传递到输出端口之类的目标(它可表示声卡上的音频输出设备)

​ 在本应用中对方发送过来的数据 会写入到源数据行中,输出到音频输出设备。

// 2.从音频流获取数据
dataLineInfo = new DataLine.Info(SourceDataLine.class, af);
sd = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
// 打开具有指定格式的行,这样可使行获得所有所需的系统资源并变得可操作。
sd.open(af);
// 允许某一数据行执行数据 I/O
sd.start();
//2.向源数据行中写入数据,写入后会自动播放
sourceDataLine.write(bts, 0, len);

3.利用socket实现通信

​ 在socket 中服务端 和 客户端 的数据传递时一致

需要注意的是 在client类和server类的run方法的while循环中 ,要先进行写、再进行读操作 ,不然socket的读取流会阻塞

代码实现
AudioUtils类 这是一个工具类
主要用来初始化 和获取SourceDataLine和TargetDataLine

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.DataLine.Info;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;

public class AudioUtils {

private static AudioFormat af;
private static Info info;
private static TargetDataLine td;
private static Info dataLineInfo;
private static SourceDataLine sd;

/**
 * 获取音频流数据(从拾音器)
 * 
 * @return TargetDataLine
 * @throws LineUnavailableException
 */
public static TargetDataLine getTargetDataLine() throws LineUnavailableException {
    if (td != null) {
        return td;
    } else {
            // 1.获取音频流数据
            // af为AudioFormat也就是音频格式
            af = getAudioFormat();
            info = new DataLine.Info(TargetDataLine.class, af);
            // 这里的td实际上是
            td = (TargetDataLine) (AudioSystem.getLine(info));
            // 打开具有指定格式的行,这样可使行获得所有所需的系统资源并变得可操作。
            td.open(af);
            // 允许某一数据行执行数据 I/O
            td.start();
        return td;
    }

}
/**
 * 获取混编器 写入数据会自动播放
 * 
 * @return SourceDataLine
 * @throws LineUnavailableException
 */
public static SourceDataLine getSourceDataLine() throws LineUnavailableException {
    if (sd != null) {
        return sd;
    } else {
            // 2.从音频流获取数据
            dataLineInfo = new DataLine.Info(SourceDataLine.class, af);
            sd = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
            // 打开具有指定格式的行,这样可使行获得所有所需的系统资源并变得可操作。
            sd.open(af);
            // 允许某一数据行执行数据 I/O
            sd.start();

        return sd;
    }
}

/**
 * 设置AudioFormat的参数
 * 
 * @return AudioFormat
 */
public static AudioFormat getAudioFormat() {
    AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED;
    float rate = 8000f;
    int sampleSize = 16;
    String signedString = "signed";
    boolean bigEndian = true;
    int channels = 1;
    return new AudioFormat(encoding, rate, sampleSize, channels, (sampleSize / 8) * channels, rate, bigEndian);
}

/**
 * 关闭资源
 */
public static void close() {
    if (td != null) {
        td.close();
    }
    if (sd != null) {
        sd.close();
    }

}

}

Server类
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;

/**

  • 主要实现局域网通讯中服务端的功能
    • @author Administrator
      */
      public class Server {
      private OutputStream out;
      private InputStream in;
      private ServerSocket serverSocket;
      private Socket socket;
      //private int counter = 1;
      private byte[] bos=new byte[2024];
      //private static ByteArrayOutputStream baos;
      private byte[] bis=new byte[2024];
    public Server() { startServer(); } private void startServer() {
    try {
    serverSocket = new ServerSocket(9000, 20);
    // 等待连接
    System.out.println(“服务端:等待连接”);
    socket = serverSocket.accept();
    out = socket.getOutputStream();
    // out.flush();
    System.out.println(“服务端:连接成功”);
    // 保持通讯
    in = socket.getInputStream(); TargetDataLine targetDataLine = AudioUtils.getTargetDataLine(); SourceDataLine sourceDataLine = AudioUtils.getSourceDataLine(); while (true) { System.out.println("server:"); /** * 这里一定要先发再收 不然socket的读取流会阻塞 */ //获取音频流 int writeLen = targetDataLine.read(bos,0,bos.length); //发 if (bos != null) { //向对方发送拾音器获取到的音频 System.out.println("rerver 发"); out.write(bos,0,writeLen); } //收 int readLen = in.read(bis); if (bis != null) { //播放对方发送来的音频 System.out.println("rerver 收"); sourceDataLine.write(bis, 0, readLen); } }} catch (Exception ex) { Logger.getLogger(Server.class.getName()).log(Level.SEVERE, null, ex); } } public static void main(String args[]) {
    new Server();
    }

}

client类
package 及时通信;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.TargetDataLine;

/**
*

  • @author Administrator
    */
    public class Client {
    private OutputStream out;
    private InputStream in;
    private Socket socket;
    private byte[] bos=new byte[2024];
    //private static ByteArrayOutputStream baos;
    private static byte[] bis=new byte[2024]; public Client() {
    startClient();
    } private void startClient() {
    try {
    //这里需要根据自己的ip修改
    socket = new Socket(“192.168.43.52”, 9000); out = socket.getOutputStream(); System.out.println("客户端:连接成功"); // 保持通讯 in = socket.getInputStream();TargetDataLine targetDataLine = AudioUtils.getTargetDataLine(); SourceDataLine sourceDataLine = AudioUtils.getSourceDataLine(); while (true) { System.out.println("Client:"); //获取音频流 int writeLen = targetDataLine.read(bos,0,bos.length); //发 if (bos != null) { //向对方发送拾音器获取到的音频 System.out.println("Client 发"); out.write(bos,0,writeLen); } //收 int readLen = in.read(bis); if (bis != null) { //播放对方发送来的音频 System.out.println("Client 收"); sourceDataLine.write(bis, 0, readLen); } }} catch (Exception ex) { Logger.getLogger(Client.class.getName()) .log(Level.SEVERE, null, ex); } } public static void main(String args[]) {
    new Client();
    }
    }
    ————————————————
    版权声明:本文为CSDN博主「AndCo」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/AndCo/article/details/77806774

php 广告点击统计代码

先来创建数据库.

CREATE TABLE IF NOT EXISTS `ad_count` (
  `ad_id` int(8) NOT NULL auto_increment,
  `ad_hit` int(8) NOT NULL default ‘0’,
  `ad_name` varchar(200) character set gb2312 default NULL,
  `ad_datetime` datetime default NULL,
  `ad_qq` varchar(15) character set gb2312 default NULL,
  `ad_url` varchar(900) character set gb2312 NOT NULL,
  `ad_price` int(8) NOT NULL,
  PRIMARY KEY  (`ad_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;

数据创建好了我们就开始写程序了.数据连接我不不写了,

<?php
 require_once(“……省了”);
 $ad_id =get_value(‘ad_id’,”);这里下面有讲
 if(is_numeric( $ad_id ) ){
 
   $ad_select=”select ad_id,ad_url from ad_count where ad_id=’$ad_id’ limit 1″;
   
   $ad_update=”Update ad_count set  ad_hit=ad_hit 1 where ad_id=’$ad_id’ limit 1″;
   
   $ad_result=mysql_query($ad_select) or die(‘id error’);
   
     if( mysql_num_rows( $ad_result) ){
     
       mysql_query($ad_update);
       
       $ad_rs=mysql_fetch_array($ad_result);
       
       header(“location:”.$ad_rs[‘ad_url’]);
       
       mysql_free_result($ad_rs);
              
     }
 }

function htmltohtml($Str){
  $temp =str_replace(chr(13),'<br/>’,$Str);
  $temp =str_replace(‘<‘,’&lt;’,$Str);
  $temp =str_replace(‘>’,’&gt;’,$Str);
  $temp =addslashes($Str);
  return $temp;
 }
 function get_value($value,$para){ 
  return htmltohtml(empty($para)?(isset($_GET[$value])?$_GET[$value]:”):(isset($_POST[$value])?$_POST[$value]:”));
 }

然后我们再在要显示广告的地方如下写.

‘<a href=/cp/ad_count.php?ad_id=2 target=_blank><img src=/ad_a/ad2.gif border=0 /></a>

Cocos Creator大厅+子游戏模式

一、前言

根据上一篇(Cocos Creator热更新),可以看出以下几点:

  • build-default目录下的main.js,为cocos creator项目的入口;
  • 热更新一文中,放置在服务器上的,仅有资源,脚本,配置等,没有入口程序,因此本文中,我们需要创造一个入口程序。
还是解释一下什么叫大厅+子游戏模式:

  1. 将大厅单独作为一个完整的项目,不同的子游戏,则为不同的项目
  2. 然后要实现不同项目之间的互调,即大厅调子游戏,或者子游戏调大厅
  3. 资源共享,共用的资源放在大厅项目中,并且子游戏中可以调用

这样做的好处:

  1. 减小上架包的体积
  2. 提高热更新的效率(打开指定子游戏,才会更新子游戏)
  3. 降低项目的耦合性(如果不共享资源,子游戏完全可以随时抽取出来作为一个单独的包使用)

二、修改子游戏

1. 添加version_generato.js
2. 构建项目
3. 在原生src下,添加 main.js 入口文件
  3.1 每次构建完项目,拷贝main.js到原生目录的src中

  main.js的内容如下:

(function () {
    'use strict';

    if (window.jsb) {
        /// 1.初始化资源Lib路径Root.
        var subgameSearchPath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/')+'ALLGame/subgame/';

        /// 2.subgame资源未映射,则初始化资源映射表,否则略过映射.
        if(!cc.HallAndSubGameGlobal.subgameGlobal){
            cc.HallAndSubGameGlobal.subgameGlobal = {};

            /// 加载settings.js
            require(subgameSearchPath + 'src/settings.js');
            var settings = window._CCSettings;
            window._CCSettings = undefined;

            if ( !settings.debug ) {
                var uuids = settings.uuids;

                var rawAssets = settings.rawAssets;
                var assetTypes = settings.assetTypes;
                var realRawAssets = settings.rawAssets = {};
                for (var mount in rawAssets) {
                    var entries = rawAssets[mount];
                    var realEntries = realRawAssets[mount] = {};
                    for (var id in entries) {
                        var entry = entries[id];
                        var type = entry[1];
                        // retrieve minified raw asset
                        if (typeof type === 'number') {
                            entry[1] = assetTypes[type];
                        }
                        // retrieve uuid
                        realEntries[uuids[id] || id] = entry;
                    }
                }

                var scenes = settings.scenes;
                for (var i = 0; i < scenes.length; ++i) {
                    var scene = scenes[i];

                    if (typeof scene.uuid === 'number') {
                        scene.uuid = uuids[scene.uuid];
                    }
                }

                var packedAssets = settings.packedAssets;
                for (var packId in packedAssets) {
                    var packedIds = packedAssets[packId];
                    for (var j = 0; j < packedIds.length; ++j) {
                        if (typeof packedIds[j] === 'number') {
                            packedIds[j] = uuids[packedIds[j]];
                        }
                    }
                }
            }

            /// 加载project.js
            var projectDir = 'src/project.js';
            if ( settings.debug ) {
                projectDir = 'src/project.dev.js';
            }
            require(subgameSearchPath + projectDir);

            /// 如果当前搜索路径没有subgame,则添加进去搜索路径。
            var currentSearchPaths = jsb.fileUtils.getSearchPaths();
            if(currentSearchPaths && currentSearchPaths.indexOf(subgameSearchPath) === -1){
                jsb.fileUtils.addSearchPath(subgameSearchPath, true);
                console.log('subgame main.js 之前未添加,添加下subgameSearchPath' + currentSearchPaths);
            }

            cc.AssetLibrary.init({
                libraryPath: 'res/import',
                rawAssetsBase: 'res/raw-',
                rawAssets: settings.rawAssets,
                packedAssets: settings.packedAssets,
                md5AssetsMap: settings.md5AssetsMap
            });

            cc.HallAndSubGameGlobal.subgameGlobal.launchScene = settings.launchScene;

            /// 将subgame的场景添加到cc.game中,使得cc.director.loadScene可以从cc.game._sceneInfos查找到相关场景
            for(var i = 0; i < settings.scenes.length; ++i){
                cc.game._sceneInfos.push(settings.scenes[i]);
            }
        }

        /// 3.加载初始场景
        var launchScene = cc.HallAndSubGameGlobal.subgameGlobal.launchScene;
        cc.director.loadScene(launchScene, null,
            function () {
                console.log('subgame main.js 成功加载初始场景' + launchScene);
            }
        );
    }
})();

ps: 不用管src外部的main.js文件

  3.2 或者 添加build-templates目录,自动在每次构建项目后生成main.js文件

这里的main.js内容和上面的内容一致

4. 执行version_generator.js文件

  生成version.manifest 和 project.mainfest。这个在上一篇中已经讲过,就不细说了。

三、拷贝res,src,version.manifest 和 project.mainfest到服务器目录下

  很明显,现在我们只是把子游戏生成了资源包,但是没有做任何热更新的操作。
接下来,就需要在大厅项目中,添加下载,更新的逻辑了。

四、在大厅项目中,添加相应逻辑

  负责下载,检测更新,更新子游戏的工具库文件内容如下:

const SubgameManager = {
    _storagePath: [],

    _getfiles: function(name, type, downloadCallback, finishCallback) {
        this._storagePath[name] = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name);
        this._downloadCallback = downloadCallback;
        this._finishCallback = finishCallback;
        this._fileName = name;

        /// 替换该地址
        var UIRLFILE = "http://192.168.200.117:8000/" + name + "/remote-assets";
        var filees = this._storagePath[name] + '/project.manifest';

        var customManifestStr = JSON.stringify({
            'packageUrl': UIRLFILE,
            'remoteManifestUrl': UIRLFILE + '/project.manifest',
            'remoteVersionUrl': UIRLFILE + '/version.manifest',
            'version': '0.0.1',
            'assets': {},
            'searchPaths': []
        });

        var versionCompareHandle = function(versionA, versionB) {
            var vA = versionA.split('.');
            var vB = versionB.split('.');
            for (var i = 0; i < vA.length; ++i) {
                var a = parseInt(vA[i]);
                var b = parseInt(vB[i] || 0);
                if (a === b) {
                    continue;
                } else {
                    return a - b;
                }
            }
            if (vB.length > vA.length) {
                return -1;
            } else {
                return 0;
            }
        };

        this._am = new jsb.AssetsManager('', this._storagePath[name], versionCompareHandle);

        if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
            this._am.retain();
        }

        this._am.setVerifyCallback(function(path, asset) {
            var compressed = asset.compressed;
            if (compressed) {
                return true;
            } else {
                return true;
            }
        });


        if (cc.sys.os === cc.sys.OS_ANDROID) {
            this._am.setMaxConcurrentTask(2);
        }

        if (type === 1) {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._updateCb.bind(this));
        } else if (type == 2) {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._checkCb.bind(this));
        } else {
            this._updateListener = new jsb.EventListenerAssetsManager(this._am, this._needUpdate.bind(this));
        }

        cc.eventManager.addListener(this._updateListener, 1);

        if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
            var manifest = new jsb.Manifest(customManifestStr, this._storagePath[name]);
            this._am.loadLocalManifest(manifest, this._storagePath[name]);
        }

        if (type === 1) {
            this._am.update();
            this._failCount = 0;
        } else {
            this._am.checkUpdate();
        }
        this._updating = true;
        cc.log('更新文件:' + filees);
    },

    // type = 1
    _updateCb: function(event) {
        var failed = false;
        let self = this;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                /*0 本地没有配置文件*/
                cc.log('updateCb本地没有配置文件');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                /*1下载配置文件错误*/
                cc.log('updateCb下载配置文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                /*2 解析文件错误*/
                cc.log('updateCb解析文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                /*3发现新的更新*/
                cc.log('updateCb发现新的更新');
                break;

            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                /*4 已经是最新的*/
                cc.log('updateCb已经是最新的');
                failed = true;
                break;

            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                /*5 最新进展 */
                self._downloadCallback && self._downloadCallback(event.getPercentByFile());
                break;


            case jsb.EventAssetsManager.ASSET_UPDATED:
                /*6需要更新*/
                break;

            case jsb.EventAssetsManager.ERROR_UPDATING:
                /*7更新错误*/
                cc.log('updateCb更新错误');
                break;

            case jsb.EventAssetsManager.UPDATE_FINISHED:
                /*8更新完成*/
                self._finishCallback && self._finishCallback(true);
                break;

            case jsb.EventAssetsManager.UPDATE_FAILED:
                /*9更新失败*/
                self._failCount++;
                if (self._failCount <= 3) {
                    self._am.downloadFailedAssets();
                    cc.log(('updateCb更新失败' + this._failCount + ' 次'));
                } else {
                    cc.log(('updateCb失败次数过多'));
                    self._failCount = 0;
                    failed = true;
                    self._updating = false;
                }
                break;

            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                /*10解压失败*/
                cc.log('updateCb解压失败');
                break;
        }

        if (failed) {
            cc.eventManager.removeListener(self._updateListener);
            self._updateListener = null;
            self._updating = false;
            self._finishCallback && self._finishCallback(false);
        }
    },

    // type = 2
    _checkCb: function(event) {
        var failed = false;
        let self = this;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                /*0 本地没有配置文件*/
                cc.log('checkCb本地没有配置文件');
                break;

            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                /*1下载配置文件错误*/
                cc.log('checkCb下载配置文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                /*2 解析文件错误*/
                cc.log('checkCb解析文件错误');
                failed = true;
                break;

            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                /*3发现新的更新*/
                self._getfiles(self._fileName, 1, self._downloadCallback, self._finishCallback);
                break;

            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                /*4 已经是最新的*/
                cc.log('checkCb已经是最新的');
                self._finishCallback && self._finishCallback(true);
                break;

            case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                /*5 最新进展 */
                break;

            case jsb.EventAssetsManager.ASSET_UPDATED:
                /*6需要更新*/
                break;

            case jsb.EventAssetsManager.ERROR_UPDATING:
                /*7更新错误*/
                cc.log('checkCb更新错误');
                failed = true;
                break;


            case jsb.EventAssetsManager.UPDATE_FINISHED:
                /*8更新完成*/
                cc.log('checkCb更新完成');
                break;

            case jsb.EventAssetsManager.UPDATE_FAILED:
                /*9更新失败*/
                cc.log('checkCb更新失败');
                failed = true;
                break;

            case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                /*10解压失败*/
                cc.log('checkCb解压失败');
                break;

        }
        this._updating = false;
        if (failed) {
            self._finishCallback && self._finishCallback(false);
        }
    },

    // type = 3
    _needUpdate: function(event) {
        let self = this;
        switch (event.getEventCode()) {
            case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                cc.log('子游戏已经是最新的,不需要更新');
                self._finishCallback && self._finishCallback(false);
                break;

            case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                cc.log('子游戏需要更新');
                self._finishCallback && self._finishCallback(true);
                break;

            // 检查是否更新出错
            case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
            case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
            case jsb.EventAssetsManager.ERROR_UPDATING:
            case jsb.EventAssetsManager.UPDATE_FAILED:
                self._downloadCallback();
                break;
        }
    },

    /**
     * 下载子游戏
     * @param {string} name - 游戏名
     * @param progress - 下载进度回调
     * @param finish - 完成回调
     * @note finish 返回true表示下载成功,false表示下载失败
     */
    downloadSubgame: function(name, progress, finish) {
        this._getfiles(name, 2, progress, finish);
    },

    /**
     * 进入子游戏
     * @param {string} name - 游戏名
     */
    enterSubgame: function(name) {
        if (!this._storagePath[name]) {
            this.downloadSubgame(name);
            return;
        }

        require(this._storagePath[name] + '/src/main.js');
    },

    /**
     * 判断子游戏是否已经下载
     * @param {string} name - 游戏名
     */
    isSubgameDownLoad: function (name) {
        let file = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'ALLGame/' + name + '/project.manifest';
        if (jsb.fileUtils.isFileExist(file)) {
            return true;
        } else {
            return false;
        }
    },

    /**
     * 判断子游戏是否需要更新
     * @param {string} name - 游戏名
     * @param isUpdateCallback - 是否需要更新回调
     * @param failCallback - 错误回调
     * @note isUpdateCallback 返回true表示需要更新,false表示不需要更新
     */
    needUpdateSubgame: function (name, isUpdateCallback, failCallback) {
        this._getfiles(name, 3, failCallback, isUpdateCallback);
    },
};

module.exports = SubgameManager;

  调用的过程如下:
    1. 判断子游戏是否已下载
    2. 已下载,判断是否需要更新
    3.1 下载游戏
    3.2 更新游戏
    4. 进入子游戏

const SubgameManager = require('SubgameManager');

cc.Class({
    extends: cc.Component,

    properties: {
        downloadBtn: {
            default: null,
            type: cc.Node
        },
        downloadLabel: {
            default: null,
            type: cc.Label
        }
    },

    onLoad: function () {
        const name = 'subgame';    
        //判断子游戏有没有下载
        if (SubgameManager.isSubgameDownLoad(name)) {
            //已下载,判断是否需要更新
            SubgameManager.needUpdateSubgame(name, (success) => {
                if (success) {
                    this.downloadLabel.string = "子游戏需要更新";
                } else {
                    this.downloadLabel.string = "子游戏不需要更新";
                }
            }, () => {
                cc.log('出错了');
            });
        } else {
            this.downloadLabel.string = "子游戏未下载";
        }

        this.downloadBtn.on('click', () => {
            //下载子游戏/更新子游戏
            SubgameManager.downloadSubgame(name, (progress) => {
                if (isNaN(progress)) {
                    progress = 0;
                }
                this.downloadLabel.string = "资源下载中   " + parseInt(progress * 100) + "%";
            }, function(success) {
                if (success) {
                    SubgameManager.enterSubgame('subgame');
                } else {
                    cc.log('下载失败');
                }
            });
        }, this);
    },
});

说到这呢,就得提一下,
如果界面设计时,从大厅点击子游戏,中间有loading的界面的话,
loading界面就应该放在大厅的工程中了。

五、测试

  打开服务——>编译大厅目录——>安装运行
注意:
    一定要生成原生apk,在真机(也可以是类似于夜神的模拟器啦)上运行测试。
结果:
    1. 第一次,本地没有子游戏,提示“游戏未下载”,下载后,无需重启,可直接进入子游戏;
    2. 修改version_generator.js中的版本号,将步骤二,再走一遍,能检测到更新,同样无需重启;
    3. 在大厅中,使用cc.sys.localStorage存储的值,在子游戏中可以获取到;

本人的一点小思考:

在研究之前,想着一定要研究一下资源共享的问题;
现在想来,既然要将子游戏独立出一个项目,自然也期望以后子游戏可以作为一个单独的apk来运行,如果共用大厅的资源,以后想抽取出来,又是一项艰巨的任务。但是这样必然会造成一定的重复资源。具体取舍,等到项目后期再协调。

作者:陌上冰火
链接:https://www.jianshu.com/p/fe54ca980384
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Cocos creator 大厅子游戏和热更新

工作中,需要把cocos creator创建的多个游戏适配到Android和ios应用中,经过调研,可以利用大厅子游戏模式实现。大厅本身作为一个游戏工程,可以有加载页面,和热加载子游戏。

热更新:
https://www.jianshu.com/p/9fc813fe9e4c

大厅子游戏:
https://www.jianshu.com/p/fe54ca980384

如何动态加载和更新子游戏:
自从jsb 3.0以来,可以用反射调用Android或者ios的代码:

const SubgameManager = require('SubgameManager');

cc.Class({
    extends: cc.Component,

    properties: {

        downloadBtn: {
            default: null,
            type: cc.Node
        },
        
        label: {
            default: null,
            type: cc.Label
        },
        // defaults, set visually when attaching this script to the Canvas
        text: 'Hello, World!'
    },

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

        var name = 'subgame';  

        if (cc.sys.OS_ANDROID == cc.sys.os) {
            name = jsb.reflection.callStaticMethod("org/cocos2dx/javascript/GameNameProvider", "getName", "()Ljava/lang/String;");
            console.log("OS_ANDROID platform provides: " + name);
        }  

        if (cc.sys.OS_IOS == cc.sys.os) {
            name = jsb.reflection.callStaticMethod("GameNameProvider", "getName");
            console.log("OS_IOS platform provides: " + name);
            
        }  

        //判断子游戏有没有下载
        if (SubgameManager.isSubgameDownLoad(name)) {
            //已下载,判断是否需要更新
            SubgameManager.needUpdateSubgame(name, (success) => {
                if (success) {
                    this.label.string = "子游戏需要更新";
                    console.log("子游戏需要更新");
                } else {
                    this.label.string = "子游戏不需要更新";
                    console.log("子游戏不需要更新");
                }
            }, () => {
                console.log('出错了');
            });
        } else {
            console.log("子游戏未下载");
            this.label.string = "子游戏未下载";
        }

        this.downloadBtn.on('click', () => {
            //下载子游戏/更新子游戏
            console.log("downloadBtn clicked");
            SubgameManager.downloadSubgame(name, (progress) => {
                if (isNaN(progress)) {
                    progress = 0;
                }
                this.label.string = "资源下载中   " + parseInt(progress * 100) + "%";
                console.log(this.label.string);
            }, function(success) {
                if (success) {
                    SubgameManager.enterSubgame(name);
                    console.log("进入子游戏");
                } else {
                    console.log('下载失败');
                }
            });
        }, this);
    },

    // called every frame
    update: function (dt) {

    },
});

Android代码:

package org.cocos2dx.javascript;

public class GameNameProvider {

    public static String getName() {
        return "subgame";
    }
}

iOS代码:

#import <Foundation/Foundation.h>

@interface GameNameProvider:NSObject {
}

+ (NSString *)getName;
@end

#import "GameNameProvider.h"
#import <Foundation/Foundation.h>

@implementation GameNameProvider

// request login
+ (NSString *) getName {
    return @"subgame";
}
@end

demo地址:
https://github.com/tigershinny/CocosHotupdate

作者:tigershin
链接:https://www.jianshu.com/p/bf76989a6c1f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Cocos Creator子游戏动态下载实现(大厅+子游戏模式)

热更新
在App开发过程中,当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。热更新就是动态下发代码,它可以使开发者在不发布新版本的情况下,修复 BUG 和发布功能,让开发者得以绕开苹果的审核机制,避免长时间的审核等待以及多次被拒造成的成本。

Cocos Creator热更新
相对于app开发,游戏开发中游戏更新相对较频繁,之前在iOS开发时,JSPatch有一套很成熟的框架来实现iOS的热补丁。
Cocos Creator也提供了一种不成熟的方案,可以实现热更新。具体可以参考官方文档:
资源热更新教程
热更新管理器 AssetsManager

在 Cocos2d-x 中,开发方式是以代码来驱动,游戏中的数据大多也是在代码中存储,除非开发者构建了自己的数据驱动框架。在 Cocos Creator 框架中,所有场景都会被序列化为纯数据,在运行时使用这些纯数据来重新构建场景,界面,动画甚至组件等元素。这种开发模式,为我们热更新提供了便利。
详细的数据驱动可以参考官方文档:2. 数据驱动

子游戏下载
主程序的修改,可以通过热更新来更新,那么为了安装包较小,可不可以把所有的子模块都动态下载呢,举个例子:JJ斗地主、网易成都棋牌都是在进入游戏时才对游戏下载。

第一种思路:
就是在Cocos Creator的一个项目中,分成不同的模块进行开发,然后使用热更新来下载每个子游戏,这样可以直接套用官方的模式。
但是一个很明显的缺点就是,多个小游戏之间耦合性太高,而且非常不利于多人协作开发

第二种思路:
因为整个项目都是数据文件,是否可以下载多个项目,通过main.js入口文件来进行跳转,答案是肯定的。

先看一下项目结构,在构建的时候会生成build文件夹,里面有main.js,是项目的入口文件,frameworks是各个平台的文件,res是资源文件,src是js代码生成的源文件

jsb_polyfill.js 文件是 JS 引擎编译出来的,包含了对 C++ 引擎的一些接口封装和 Entity Component 层的代码。在不同版本的引擎中,它的代码会产生比较大的差异,而 C++ 底层也会随之发生一些改变。
project.js,所有的js代码会打包到project.js文件中,在非调试模式时是project.dev.js
setting.js文件是所有资源文件的配置文件,通过uuid进行索引

如果子项目不包括原生代码的话,project.js setting.js 以及res文件夹就包含了所有代码和资源,如果加上main.js的入口文件,那么把这些文件整个打包下载下来,就可以实现子游戏的热更新,但是需要对部分js文件进行修改

具体实现方案
子游戏修改:
1.修改子游戏的main.js文件,并放入 build-templates/jsb-default/src 文件夹中,该文件为跳转入口文件,但是并不替换子游戏的 main.js,因此子游戏独立运行并没有影响
main.js文件的主要改动是从子游戏的路径中读取配置文件等
2.通过Cocos Creator提供的热更新方法,打包文件后, 把src和res文件夹通过version_generator.js生成manifest文件,把生成的两个文件以及src和res文件夹上传到远端服务器中

主程序修改:
1.修改大厅 jsb_polyfill.js 文件,并放入 build-templates/jsb-default/src 文件夹中,jsb_polyfill.js文件和 Creator 的版本有关,因为升级 Creator 时需要重新修改
jsb_polyfill.js修改的目的是,为了实现大厅和子游戏资源文件的共享,因为 cc.AssetLibrary 的实现是会覆盖之前的资源映射关系,因此需要修改系统文件,这样在子游戏中也可以调用大厅的资源文件了

2.也要修改main.js文件
3.实现热更新的方法,具体可以参考官方实现

Demo:https://github.com/wf96390/CocosHotupdateDemo

Demo部署:

  1. 可以使用subgame.sh进行打包,打包文件生成到hotupdate目录中
  2. 如果部署到本地的话,默认是http://127.0.0.1/subgame/目录,你也可以进行修改(打包路径+代码中的热更新路径修改),把上一步生成的src, res, project.manifest, version.manifest复制到服务器subgame文件夹中

参考:
http://forum.cocos.com/t/topic/54899
http://forum.cocos.com/t/topic/53115
————————————————
版权声明:本文为CSDN博主「-慢慢-」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wf96390/java/article/details/80815430

Swoole Timer 的应用

这是关于 Swoole 学习的第一篇文章:Swoole Timer 的应用。

PHP 的协程高性能网络通信引擎,使用 C/C++ 语言编写,提供了多种通信协议的网络服务器和客户端模块。

Swoole 可应用于互联网、移动通信、企业软件、网络游戏、物联网、车联网、智能家庭等领域。

学习 Swoole 之前,最好先了解下底层知识,比如,线程/进程、IO、TCP/IP协议 等。

推荐大家读一下《Linux 高性能服务器编程》这本书。我有这本书的PDF版,需要的可以关注公众号,回复 “Linux 高性能服务器编程” 即可获取。

这篇文章主要分享 Timer 毫秒精度的定时器。

本地版本:PHP 7.2.6、Swoole 4.3.1。

Timer

主要有三个方法:

swoole_timer_tick 间隔的时钟控制器

swoole_timer_after 指定的时间后执行

swoole_timer_clear 删除定时器

示例代码:

//每隔3000ms触发一次
$timer_id = swoole_timer_tick(3000, function () {
    echo "tick 3000ms - ".date('Y-m-d H:i:s')."\n";
});

//9000ms后删除定时器
swoole_timer_after(9000, function () use ($timer_id) {
    echo "after 9000ms - ".date('Y-m-d H:i:s')."\n";
    swoole_timer_clear($timer_id);
});

运行结果:

tick 3000ms - 2019-04-07 21:36:56
tick 3000ms - 2019-04-07 21:36:59
tick 3000ms - 2019-04-07 21:37:02
after 9000ms - 2019-04-07 21:37:02

应用场景

一、比如,每天凌晨跑业务脚本,脚本中包括了请求其他业务方或第三方的接口,如果接口超时无响应或没有数据返回,需要进行重试。

重试机制为:每5隔分钟再发送一次请求,最多尝试5次,在5次内成功停止该任务,5次仍失败也停止该任务。

示例代码:

$api_url  = 'xxx'; //接口地址
$exec_num = 0;     //执行次数
swoole_timer_tick(5*60*1000, function($timer_id) use ($api_url, &$exec_num) {
    $exec_num ++ ;
    $result = $this->requestUrl($api_url);
    echo date('Y-m-d H:i:s'). " 执行任务中...(".$exec_num.")\n";
    if ($result) {
        //业务代码...
        swoole_timer_clear($timer_id); // 停止定时器
        echo date('Y-m-d H:i:s'). " 第(".$exec_num.")次请求接口任务执行成功\n";
    } else {
        if ($exec_num >= 5) {
            swoole_timer_clear($timer_id); // 停止定时器
            echo date('Y-m-d H:i:s'). " 请求接口失败,已失败5次,停止执行\n";
        } else {
            echo date('Y-m-d H:i:s'). " 请求接口失败,5分钟后再次尝试\n";
        }
    }
});

运行结果:

2019-04-07 21:40:48 执行任务中...(1)
2019-04-07 21:40:48 请求接口失败,5分钟后再次尝试
2019-04-07 21:45:48 执行任务中...(2)
2019-04-07 21:45:48 请求接口失败,5分钟后再次尝试
2019-04-07 21:50:48 执行任务中...(3)
2019-04-07 21:50:48 请求接口失败,5分钟后再次尝试
2019-04-07 21:55:48 执行任务中...(4)
2019-04-07 21:55:48 请求接口失败,5分钟后再次尝试
2019-04-07 22:00:48 执行任务中...(5)
2019-04-07 22:00:48 请求接口失败,已失败5次,停止执行

二、比如,设计一个用WEB界面管理管理定时任务的系统。

Linux Crontab 最小时间粒度为分钟。

PHP Swoole 最小时间粒度为毫秒。

0   1   2   3   4   5
|   |   |   |   |   |
|   |   |   |   |   +------ day of week (0 - 6) (Sunday=0)
|   |   |   |   +------ month (1 - 12)
|   |   |   +-------- day of month (1 - 31)
|   |   +---------- hour (0 - 23)
|   +------------ min (0 - 59)
+-------------- sec (0-59)

WEB界面管理

  • 登录、权限管理
  • 任务管理(增删改查)
  • 脚本机管理(机器IP地址)
  • 任务日志

项目地址

https://github.com/osgochina/Donkey

三、比如,监控服务器状况。

参考文档

本文欢迎转发,转发请注明作者和出处,谢谢!

(最佳方案)cocos creator~客户端WebSocket与 node.js ws模块通信

1)node.js服务端ws模块
var ws = require(“ws”); var server = new ws.Server({ port: 6080 });
server.on(“connection”, function (session) { session.on(“close”, function () {
}); session.on(“error”, function (err) { }); session.on(“message”, function
(data) { console.log(“server rcv data=” + data); session.send(data); }); });

2)客户端websocket.js封装
var websocket = { sock: null, on_open: function () {
this.send_data(JSON.stringify({ stype: “auth”, ctype: “login”, data: { name:
“jianan”, pwd: 123456 } })); }, on_message: function (event) {
console.log(“client rcv data=” + event.data); }, on_close: function () {
this.close(); }, on_error: function () { this.close(); }, close: function () {
if(this.sock){ this.sock.close(); this.sock = null; } }, connect: function
(url) { this.sock = new WebSocket(url); this.sock.binaryType = “arraybuffer”;
this.sock.onopen = this.on_open.bind(this); this.sock.onmessage =
this.on_message.bind(this); this.sock.onclose = this.on_close.bind(this);
this.sock.onerror = this.on_error.bind(this); }, send_data: function (data) {
this.sock.send(data); } } module.exports = websocket;
cocos creator客户端连接node.js服务器(此处通信用json协议)
var websocket = require(“./websocket.js”); cc.Class({ extends: cc.Component,
properties: { }, // LIFE-CYCLE CALLBACKS: // onLoad () {}, start () {
websocket.connect(“ws://127.0.0.1:6080/ws”); }, // update (dt) {}, });

总结:

socket.io则注意事项略多,特别需要客户端服务器版本号一致,容易出问题。 推荐使用websocket,而不要使用socket.io !!!

swoole Timer 毫秒定时器

定时器的原理:

 定时器在实际的开发中非常常用,列如crontab就是一个非常常用的定时器,可以在指定的时间执行规定的任务,PHP本身也提pcntl_alarm,通过模拟时钟信号实现定时器,不过这些定时器都有一个缺点,不能精确到毫秒,还有比如执行一个任务,在某些特定的场合想关闭定时器,传统的php也实现不了这些功能。而swoole 弥补了这些缺点。

swoole 的 swoole_timer模块提供了一个毫秒定时器,swoole_timer是基于timerfd+epoll实现的异步毫秒定时器,可完美的运行在EventLoop中,与swoole_client/swoole_event等模块可以无缝结合。

定时器的使用:

  在swoole 的定时器分为两种:一种是长类型的定时器tick,这种定时器一旦被创建,就会按照一定的时间执行任务,除非清除定时器,而另外一种定时器 after是临时定时器,这个定时器仅仅只运作一次,在指定的时间内响应一次,就会自动清除。

1、tick 定时器

    创建一个 tick  定时器的原型 如下:

int swoole_timer_tick(int $ms, mixed $callback, mixed $param = null);int swoole_server::tick(int $ms, mixed $callback, mixed $param = null);

回调函数的原型如下所示: php function onTimer(int $timer_id, mixed $params = null); 第一个参数是响应的timer_id,对应调用tick函数返回的id;第二个参数是调用tick时传入的第三个参数的值。我们可以用这个param参数传递一些公共参数进去(因为这个参数一旦给定,就不能在后续的逻辑中改变了)。另外,也可以使用PHP的闭包(use关键字)实现传参。具体实例如下:

参数名参数说明$ms定时器的间隔,单位是ms$callback回调函数,定时器响应后执行此函数$param用户自定义参数,会被传递给callback回调

tick函数会返回定时器的id。当我们不再需要某个定时器的时候,可以根据这个id,调用swoole_timer_clear函数删除定时器。

2、after 定时器

 创建一个 after 定时器的原型 如下:

int swoole_timer_after(int $after_time_ms, mixed $callback_function, mixed $user_param);

回调函数的原型如下所示:

function onTimer() // 回调函数的原型,不接受任何参数

after定时器的回调函数不接受任何参数,可以通过闭包方式传递参数,也可以通过类成员变量的方式传递。具体实例如下:

class Test{private $str = "Say Hello";public function onAfter(){echo $this->str; // 输出”Say Hello“}}$test = new Test();swoole_timer_after(1000, array($test, "onAfter"); // 成员变量swoole_timer_after(2000, function() use($test){ // 闭包$test->onAfter(); // 输出”Say Hello“});


after定时器适用于某些延后执行的操作,比如用户创建了一个订单需要在5分钟后检查这个订单是否被支付等。这些任务往往需要在间隔一定时间后执行,并且只需要执行一次,就可以直接使用after来实现了。

Cocos Creator 快速上手:制作第一个游戏—–二次开发

快速上手:制作第一个游戏
您正在阅读的手册文档包括了系统化的介绍 Cocos Creator 的编辑器界面、功能和工作流程,但如果您想快速上手体验使用 Cocos Creator 开发游戏的大体流程和方法,这一章将满足您的好奇心。完成本章教程之后,您应该能获得足够上手制作游戏的信息,不过我们还是推荐您继续阅读本手册来了解各个功能模块的细节和完整的工作流程。

接下来就让我们开始吧,跟随教程我们将会制作一款名叫摘星星的坑爹小游戏。这款游戏的玩家要操作一个反应迟钝却蹦跳不停的小怪物来碰触不断出现的星星,难以驾驭的加速度将给玩家带来很大挑战,和您的小伙伴比比谁能拿到更多星星吧!

可以在这里感受一下这款游戏的完成形态:

http://fbdemos.leanapp.cn/star-catcher/

准备项目和资源
我们已经为您准备好了制作这款游戏需要的全部资源,下载初始项目后,解压到您希望的位置,之后我们就可以开始了:

下载初始项目

您也可以下载最终完成的项目,跟随教程制作过程中有任何不明白的地方都可以打开完成版的项目作为参考:

下载完成项目

打开初始项目
如果您还不了解如何获取和启动 Cocos Creator,请阅读安装和启动一节。

我们首先启动 Cocos Creator,然后选择打开其他项目
在弹出的文件夹选择对话框中,选中我们刚下载并解压完成的 start_project,点击打开按钮
Cocos Creator 编辑器主窗口会打开,我们将看到如下的项目状态

检查游戏资源
我们的初始项目中已经包含了所有需要的游戏资源,因此您不需要再导入任何其他资源。如果希望了解导入资源的方法,可以阅读资源工作流程的相关内容。

下面我们先来了解一下项目中都有哪些资源,请关注名为 资源管理器 的面板,这里显示的是项目中的所有资源树状结构。

可以看到,项目资源的根目录名叫assets,对应我们解压之后初始项目中的 assets 目录,只有这个目录下的资源才会被 Cocos Creator 导入项目并进行管理。

资源管理器可以显示任意层次的目录结构,我们可以看到这样的图标就代表一个文件夹,点击文件夹左边的三角图标可以展开文件夹的内容。将文件夹全部展开后, 资源管理器 中就会呈现如下图的状态。

每个资源都是一个文件,导入项目后根据扩展名的不同而被识别为不同的资源类型,其图标也会有所区别,下面我们来看看项目中的资源各自的类型和作用:

 声音文件,一般为 mp3 文件,我们将在主角跳跃和得分时播放名为jump和score的声音文件。
 位图字体,由 fnt 文件和同名的 png 图片文件共同组成。位图字体(Bitmap Font)是一种游戏开发中常用的字体资源,详情请阅读位图字体资源
各式各样的缩略图标,这些都是图像资源,一般是 png 或 jpg 文件。图片文件导入项目后会经过简单的处理成为texture类型的资源。之后就可以将这些资源拖拽到场景或组件属性中去使用了。
创建游戏场景
在 Cocos Creator 中,游戏场景(Scene)是开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体。游戏场景中一般会包括以下内容:

场景图像和文字(Sprite,Label)
角色
以组件形式附加在场景节点上的游戏逻辑脚本
当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本,实现各种各样开发者设置的逻辑功能。所以除了资源以外,游戏场景是一切内容创作的基础,让我们现在就新建一个场景。

在资源管理器中点击选中assets目录,确保我们的场景会被创建在这个目录下
点击资源管理器左上角的加号按钮,在弹出的菜单中选择 Scene

我们创建了一个名叫New Scene的场景文件,右键点击它并选择重命名,将它改名为game。

双击game,就会在 场景编辑器 和层级编辑器中打开这个场景。
了解Canvas
打开场景后, 层级管理器 中会显示当前场景中的所有节点和他们的层级关系。我们刚刚新建的场景中只有一个名叫Canvas的节点,Canvas可以被称为画布节点或渲染根节点,点击选中Canvas,可以在 属性检查器 中看到他的属性。

这里的Design Resolution属性规定了游戏的设计分辨率,Fit Height和Fit Width规定了在不同尺寸的屏幕上运行时,我们将如何缩放Canvas以适配不同的分辨率。

由于提供了多分辨率适配的功能,我们一般会将场景中的所有负责图像显示的节点都放在Canvas下面。这样当Canvas的scale(缩放)属性改变时,所有作为其子节点的图像也会跟着一起缩放以适应不同屏幕的大小。

更详细的信息请阅读Canvas组件参考。目前我们只要知道接下来添加的场景图像都会放在Canvas节点下面就可以了。

设置场景图像
添加背景
首先在 资源管理器 里按照assets/textures/background的路径找到我们的背景图像资源,点击并拖拽这个资源到 层级编辑器 中的Canvas节点上,直到Canvas节点显示橙色高亮,表示将会添加一个以background为贴图资源的子节点。

这时就可以松开鼠标按键,可以看到Canvas下面添加了一个名叫background的节点。当我们使用拖拽资源的方式添加节点时,节点会自动以贴图资源的文件名来命名。

我们在对场景进行编辑修改时,可以通过主菜单文件->保存场景来及时保存我们的修改。也可以使用快捷键Ctrl+S(Windows)或Cmd + S(Mac)来保存。

修改背景尺寸

在 场景编辑器 中,可以看到我们刚刚添加的背景图像,下面我们将修改背景图像的尺寸,来让他覆盖整个屏幕。

首先选中background节点,然后点击主窗口左上角工具栏第四个 矩形变换工具:

使用这个工具我们可以方便的修改图像节点的尺寸,将鼠标移动到 场景编辑器 中 background 的左边,按住并向左拖拽直到 background 的左边超出表示设计分辨率的蓝色线框。然后再用同样的方法将 background 的右边向右拖拽。

之后需要拖拽上下两边,使背景图的大小能够填满设计分辨率的线框。

在使用 矩形变换工具 修改背景图尺寸时,可以在 属性检查器 中看到 Node (节点)中的 Size 属性也在随之变化,完成后我们的背景图尺寸大概是 (1360, 760)。您也可以直接在 Size 属性的输入框中输入数值,和使用 矩形变换工具 可以达到同样效果。这样大小的背景图在市面流行的手机上都可以覆盖整个屏幕,不会出现穿帮情况。

添加地面
我们的主角需要一个可以在上面跳跃的地面,我们马上来添加一个。用和添加背景图相同的方式,拖拽 资源管理器 中 assets/textures/ground 资源到 层级管理器 的 Canvas 上。这次在拖拽时我们还可以选择新添加的节点和 background节点的顺序关系。拖拽资源的状态下移动鼠标指针到 background 节点的下方,直到在 Canvas 上显示橙色高亮框,并同时在 background 下方显示表示插入位置的绿色线条,然后松开鼠标,这样 ground 在场景层级中就被放在了 background 下方,同时也是 Canvas 的一个子节点。

在 层级管理器 中,显示在下方的节点的渲染顺序是在上方节点的后面,也就是说下方的节点是在上方节点之后绘制的,我们可以看到位于最下的ground物体在 场景编辑器 中显示在了最前。另外子节点也会永远显示在父节点之前,我们可以随时调整节点的层级顺序和关系来控制他们的显示顺序。

按照修改背景的方法,我们也可以使用矩形变换工具来为地面节点设置一个合适的大小。在激活矩形变换工具的时候,如果拖拽节点顶点和四边之外的部分,就可以更改节点的位置。下图是我们设置好的地面节点状态:

除了矩形变换工具之外,我们还可以使用移动工具来改变节点的位置。尝试按住移动工具显示在节点上的箭头并拖拽,就可以一次改变节点在单个坐标轴上的位置。

我们在设置背景和地面的位置和尺寸时不需要很精确的数值,可以凭感觉拖拽。如果您偏好比较完整的数字,也可以按照截图直接输入position和size的数值。

添加主角
接下来我们的主角小怪兽要登场了,从 资源管理器 拖拽assets/texture/PurpleMonster到 层级管理器 中 Canvas 的下面,并确保他的排序在 ground 之下,这样我们的主角会显示在最前面。 注意小怪兽节点应该是 Canvas 的子节点,和 ground 节点平行。

为了让主角的光环在场景节点中非常醒目,我们右键点击刚刚添加的PurpleMonster节点,选择重命名之后将其改名为Player。

接下来我们要对主角的属性进行一些设置,首先是改变锚点(Anchor)的位置。默认状态下,任何节点的锚点都会在节点的中心,也就是说该节点中心点所在的位置就是该节点的位置。我们希望控制主角的底部的位置来模拟在地面上跳跃的效果,所以现在我们需要把主角的锚点设置在脚下。在 属性检查器 里找到Anchor属性,把其中的y值设为0,可以看到 场景编辑器 中,表示主角位置的移动工具的箭头出现在了主角脚下。

接下来 场景编辑器 中拖拽Player,把他放在地面上,效果如下图:

这样我们基本的场景美术内容就配置好了。下面一节我们要编写代码让游戏里的内容生动起来。

编写主角脚本
Cocos Creator 开发游戏的一个核心理念就是让内容生产和功能开发可以流畅的并行协作,我们在上个部分着重于处理美术内容,而接下来就是通过编写脚本来开发功能的流程,之后我们还会看到写好的程序脚本可以很容易的被内容生产者使用。

如果您从没写过程序也不用担心,我们会在教程中提供所有需要的代码,只要复制粘贴到正确的位置就可以了,之后这部分工作可以找您的程序员小伙伴来解决。下面让我们开始创建驱动主角行动的脚本吧。

创建脚本
首先在 资源管理器 中右键点击assets文件夹,选择新建->文件夹
右键点击New Folder,选择重命名,将其改名为scripts,之后我们所有的脚本都会存放在这里。
右键点击scripts文件夹,选择新建->JavaScript,创建一个JavaScript脚本
将新建脚本的名字改为Player。双击这个脚本,打开代码编辑器。
注意: Cocos Creator 中脚本名称就是组件的名称,这个命名是大小写敏感的!如果组件名称的大小写不正确,将无法正确通过名称使用组件!

编写组件属性
打开的脚本里已经有了预先设置好的一些代码块,这些代码就是编写一个组件脚本所需的结构。具有这样结构的脚本就是 Cocos Creator 中的组件(Component),他们能够挂载到场景中的节点上,提供控制节点的各种功能。我们先来设置一些属性,然后看看怎样在场景中调整他们。

在 代码编辑器 中找到Player脚本里 properties部分,将其改为以下内容并按Ctrl + S(Windows)或Cmd + S(Mac)保存:

// Player.js
//…
properties: {
// 主角跳跃高度
jumpHeight: 0,
// 主角跳跃持续时间
jumpDuration: 0,
// 最大移动速度
maxMoveSpeed: 0,
// 加速度
accel: 0,
},
//…
这些新增的属性将规定主角的移动方式,在代码中我们不需要关心这些数值是多少,因为我们之后会直接在 属性检查器 中设置这些数值。

现在我们可以把 Player 组件添加到主角节点上。在 层级编辑器 中选中 Player 节点,然后在 属性检查器 中点击 添加组件 按钮,选择 添加用户脚本组件->Player,为主角节点添加 Player 组件。

现在我们可以在 属性检查器 中(需要选中 Player 节点)看到刚添加的 Player 组件了,按照下图将主角跳跃和移动的相关属性设置好:

这些数值除了 jumpDuration 的单位是秒之外,其他的数值都是以像素为单位的,根据我们现在对 Player 组件的设置:我们的主角将能够跳跃 200 像素的高度,起跳到最高点所需的时间是 0.3 秒,最大水平方向移动速度是 400 像素每秒,水平加速度是 350 像素每秒。

这些数值都是建议,一会等游戏运行起来,您完全可以按照自己的喜好随时在 属性检查器 中修改这些数值,不需要改动任何代码,很方便吧!

编写跳跃和移动代码
下面我们添加一个方法,来让主角跳跃起来,在 properties: {…},代码块的下面,添加叫做setJumpAction的方法:

// Player.js
properties: {
//…
},

setJumpAction: function () {
    // 跳跃上升
    var jumpUp = cc.moveBy(this.jumpDuration, cc.p(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
    // 下落
    var jumpDown = cc.moveBy(this.jumpDuration, cc.p(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
    // 不断重复
    return cc.repeatForever(cc.sequence(jumpUp, jumpDown));
},

这里用到了一些 Cocos2d-js 引擎中的 Action 来实现主角的跳跃动画,详情可以查询Cocos2d-js API.

接下来在onLoad方法里调用刚添加的setJumpAction方法,然后执行runAction来开始动作:

// Player.js
onLoad: function () {
// 初始化跳跃动作
this.jumpAction = this.setJumpAction();
this.node.runAction(this.jumpAction);
},
onLoad方法会在场景加载后立刻执行,所以我们会把初始化相关的操作和逻辑都放在这里面。

保存脚本,然后我们就可以开始第一次运行游戏了!

点击 Cocos Creator 编辑器上方正中的预览游戏按钮,Cocos Creator 会自动打开您的默认浏览器并开始在里面运行游戏,现在应该可以看到我们的主角——紫色小怪兽在场景中间活泼的蹦个不停了。

移动控制
只能在原地傻蹦的主角可没前途,让我们为主角添加键盘输入,用A和D来控制他的跳跃方向。在setJumpAction方法的下面添加setInputControl方法:

// Player.js
setJumpAction: function () {
//…
},

setInputControl: function () {
    var self = this;
    // 添加键盘事件监听
    // 有按键按下时,判断是否是我们指定的方向控制键,并设置向对应方向加速
    cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, function (event){
        switch(event.keyCode) {
            case cc.KEY.a:
                self.accLeft = true;
                break;
            case cc.KEY.d:
                self.accRight = true;
                break;
        }
    });

    // 松开按键时,停止向该方向的加速
    cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, function (event){
        switch(event.keyCode) {
            case cc.KEY.a:
                self.accLeft = false;
                break;
            case cc.KEY.d:
                self.accRight = false;
                break;
        }
    });
},

然后修改onLoad方法,在其中加入向左和向右加速的开关,以及主角当前在水平方向的速度,最后再调用我们刚添加的setInputControl方法,在场景加载后就开始监听键盘输入:

// Player.js
onLoad: function () {
// 初始化跳跃动作
this.jumpAction = this.setJumpAction();
this.node.runAction(this.jumpAction);

    // 加速度方向开关
    this.accLeft = false;
    this.accRight = false;
    // 主角当前水平方向速度
    this.xSpeed = 0;

    // 初始化键盘输入监听
    this.setInputControl();
},

最后修改update方法的内容,添加加速度、速度和主角当前位置的设置:

// Player.js
update: function (dt) {
// 根据当前加速度方向每帧更新速度
if (this.accLeft) {
this.xSpeed -= this.accel * dt;
} else if (this.accRight) {
this.xSpeed += this.accel * dt;
}
// 限制主角的速度不能超过最大值
if ( Math.abs(this.xSpeed) > this.maxMoveSpeed ) {
// if speed reach limit, use max speed with current direction
this.xSpeed = this.maxMoveSpeed * this.xSpeed / Math.abs(this.xSpeed);
}

    // 根据当前速度更新主角的位置
    this.node.x += this.xSpeed * dt;
},

update在场景加载后就会每帧调用一次,我们一般把需要经常计算或及时更新的逻辑内容放在这里。在我们的游戏中,根据键盘输入获得加速度方向后,就需要每帧在update中计算主角的速度和位置。

保存脚本后,下面就可以去泡杯茶,点击预览游戏来看看我们最新的成果。在浏览器打开预览后,用鼠标点击一下游戏画面(这是浏览器的限制,要点击游戏画面才能接受键盘输入),然后就可以按A和D键来控制主角左右移动了!

感觉移动起来有点迟缓?主角跳的不够高?希望跳跃时间长一些?没问题,这些都可以随时调整。只要为Player组件设置不同的属性值,就可以按照您的想法调整游戏。这里有一组设置可供参考:

Jump Height: 150
Jump Duration: 0.3
Max Move Speed: 400
Accel: 1000
这组属性设置会让主角变得灵活无比,至于如何选择,就看您想做一个什么风格的游戏了。

制作星星
主角现在可以跳来跳去了,我们要给玩家一个目标,也就是会不断出现在场景中的星星,玩家需要引导小怪兽碰触星星来收集分数。被主角碰到的星星会消失,然后马上在随机位置重新生成一个。

制作Prefab
对于需要重复生成的节点,我们可以将他保存成 Prefab(预制) 资源,作为我们动态生成节点时使用的模板。关于 Prefab的更多信息,请阅读 预制资源(Prefab)。

首先从 资源管理器 中拖拽 assets/textures/star 图片到场景中,位置随意,我们只是需要借助场景作为我们制作 Prefab 的工作台,制作完成后会我们把这个节点从场景中删除。

我们不需要修改星星的位置或渲染属性,但要让星星能够被主角碰触后消失,我们需要为星星也添加一个专门的组件。按照和添加 Player 脚本相同的方法,添加名叫 Star 的 JavaScript 脚本到 assets/scripts/ 中。

接下来双击这个脚本开始编辑,星星组件只需要一个属性用来规定主角距离星星多近时就可以完成收集,修改 properties,加入以下内容:

// Star.js
properties: {
// 星星和主角之间的距离小于这个数值时,就会完成收集
pickRadius: 0,
// 暂存 Game 对象的引用
game: {
default: null,
serializable: false
}
},
保存脚本后,将这个脚本添加到刚创建的 star 节点上。然后在 属性检查器 中把 Pick Radius 属性值设为 60:

Star Prefab 需要的设置就完成了,现在从 层级管理器 中将 star 节点拖拽到 资源管理器 中的 assets 文件夹下,就生成了名叫 star 的 Prefab 资源。

现在可以从场景中删除 star 节点了,后续可以直接双击这个 star Prefab 资源进行编辑。

接下去我们会在脚本中动态使用星星的 Prefab 资源生成星星。

添加游戏控制脚本
星星的生成是游戏主逻辑的一部分,所以我们要添加一个叫做Game的脚本作为游戏主逻辑脚本,这个脚本之后还会添加计分、游戏失败和重新开始的相关逻辑。

添加Game脚本到assets/scripts文件夹下,双击打开脚本。首先添加生成星星需要的属性:

// Game.js
properties: {
// 这个属性引用了星星预制资源
starPrefab: {
default: null,
type: cc.Prefab
},
// 星星产生后消失时间的随机范围
maxStarDuration: 0,
minStarDuration: 0,
// 地面节点,用于确定星星生成的高度
ground: {
default: null,
type: cc.Node
},
// player 节点,用于获取主角弹跳的高度,和控制主角行动开关
player: {
default: null,
type: cc.Node
}
},
保存脚本后将Game组件添加到 层级编辑器 中的Canvas节点上(选中Canvas节点后,拖拽脚本到 属性检查器 上,或点击 属性检查器 的 添加组件 按钮,并从 用户自定义脚本 中选择 Game,接下来从 资源管理器 中拖拽star Prefab 资源到Game组件的Star Prefab属性中。这是我们第一次为属性设置引用,只有在属性声明时规定type为引用类型时(比如我们这里写的cc.Prefab类型),才能够将资源或节点拖拽到该属性上。

接下来从 层级编辑器 中拖拽ground和Player 节点到组件中相同名字的属性上,完成节点引用。

然后设置Min Star Duration和Max Star Duration属性的值为3和5,之后我们生成星星时,会在这两个之间随机取值,就是星星消失前经过的时间。

在随机位置生成星星
接下来我们继续修改Game脚本,在onLoad方法后面添加生成星星的逻辑:

// Game.js
onLoad: function () {
// 获取地平面的 y 轴坐标
this.groundY = this.ground.y + this.ground.height/2;
// 生成一个新的星星
this.spawnNewStar();
},

spawnNewStar: function() {
    // 使用给定的模板在场景中生成一个新节点
    var newStar = cc.instantiate(this.starPrefab);
    // 将新增的节点添加到 Canvas 节点下面
    this.node.addChild(newStar);
    // 为星星设置一个随机位置
    newStar.setPosition(this.getNewStarPosition());
},

getNewStarPosition: function () {
    var randX = 0;
    // 根据地平面位置和主角跳跃高度,随机得到一个星星的 y 坐标
    var randY = this.groundY + cc.random0To1() * this.player.getComponent('Player').jumpHeight + 50;
    // 根据屏幕宽度,随机得到一个星星 x 坐标
    var maxX = this.node.width/2;
    randX = cc.randomMinus1To1() * maxX;
    // 返回星星坐标
    return cc.p(randX, randY);
}

保存脚本以后点击预览游戏按钮,在浏览器中可以看到,游戏开始后动态生成了一颗星星!用同样的方法,您可以在游戏中动态生成任何预先设置好的以 Prefab 为模板的节点。

添加主角碰触收集星星的行为
现在要添加主角收集星星的行为逻辑了,这里的重点在于,星星要随时可以获得主角节点的位置,才能判断他们之间的距离是否小于可收集距离,如何获得主角节点的引用呢?别忘了我们前面做过的两件事:

Game组件中有个名叫player的属性,保存了主角节点的引用。
每个星星都是在Game脚本中动态生成的。
所以我们只要在Game脚本生成Star节点实例时,将Game组件的实例传入星星并保存起来就好了,之后我们可以随时通过game.player来访问到主角节点。让我们打开Game脚本,在spawnNewStar方法最后面添加这样一句:

// Game.js
spawnNewStar: function() {
// …
// 将 Game 组件的实例传入星星组件
newStar.getComponent(‘Star’).game = this;
},
保存后打开Star脚本,现在我们可以利用Game组件中引用的player节点来判断距离了,在onLoad方法后面添加名为getPlayerDistance和onPicked的方法:

// Star.js
getPlayerDistance: function () {
// 根据 player 节点位置判断距离
var playerPos = this.game.player.getPosition();
// 根据两点位置计算两点之间距离
var dist = cc.pDistance(this.node.position, playerPos);
return dist;
},

onPicked: function() {
    // 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星
    this.game.spawnNewStar();
    // 然后销毁当前星星节点
    this.node.destroy();
},

然后在update方法中添加每帧判断距离,如果距离小于pickRadius属性规定的收集距离,就执行收集行为:

// Star.js
update: function (dt) {
// 每帧判断和主角之间的距离是否小于收集距离
if (this.getPlayerDistance() < this.pickRadius) {
// 调用收集行为
this.onPicked();
return;
}
},
保存脚本,然后再次预览测试,可以看到控制主角靠近星星时,星星就会消失掉,然后在随机位置生成了新的星星!

添加得分
小怪兽辛辛苦苦的收集星星,没有奖励怎么行,让我们现在就在收集星星时添加得分奖励的逻辑和显示。

添加分数文字(Label)
游戏开始时得分从0开始,每收集一个星星分数就会加1。要显示得分,首先要创建一个 Label 节点。在 层级管理器 中选中Canvas节点,右键点击并选择菜单中的创建新节点->创建渲染节点->Label(文字),一个新的 Label 节点会被创建在Canvas下面,而且顺序在最下面。接下来我们要用如下的步骤配置这个 Label 节点:

将该节点名字改为score
将score节点的位置(position属性)设为(0, 180)。
选中该节点,编辑Label组件的string属性,填入Score: 0的文字。
将Label组件的Font Size属性设为50。
从 资源管理器 中拖拽assets/mikado_outline_shadow位图字体资源(注意图标是)到Label组件的Font属性中,将文字的字体替换成我们项目资源中的位图字体。
完成后效果如下图所示:

在 Game 脚本中添加得分逻辑
我们将会把计分和更新分数显示的逻辑放在Game脚本里,打开Game脚本开始编辑,首先在properties区块的最后添加分数显示 Label 的引用属性:

// Game.js
properties: {
// …
// score label 的引用
scoreDisplay: {
default: null,
type: cc.Label
}
},
接下来在onLoad方法里添加计分用的变量的初始化:

// Game.js
onLoad: function () {
// …
// 初始化计分
this.score = 0;
},
然后在update方法后面添加名叫gainScore的新方法:

// Game.js
gainScore: function () {
this.score += 1;
// 更新 scoreDisplay Label 的文字
this.scoreDisplay.string = ‘Score: ‘ + this.score.toString();
},
保存 Game 脚本后,回到 层级管理器,选中 Canvas 节点,然后把前面添加好的 score 节点拖拽到 属性检查器 里 Game 组件的 Score Display 属性中。

在 Star 脚本中调用 Game 中的得分逻辑
下面打开Star脚本,在onPicked方法中加入gainScore的调用:

// Star.js
onPicked: function() {
// 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星
this.game.spawnNewStar();
// 调用 Game 脚本的得分方法
this.game.gainScore();
// 然后销毁当前星星节点
this.node.destroy();
},
保存后预览,可以看到现在收集星星时屏幕正上方显示的分数会增加了!

失败判定和重新开始
现在我们的游戏已经初具规模,但得分再多,不可能失败的游戏也不会给人成就感。现在让我们加入星星定时消失的行为,而且让星星消失时就判定为游戏失败。也就是说,玩家需要在每颗星星消失之前完成收集,并不断重复这个过程完成玩法的循环。

为星星加入计时消失的逻辑
打开Game脚本,在onLoad方法的spawnNewStar调用之前加入计时需要的变量声明:

// Game.js
onLoad: function () {
// …
// 初始化计时器
this.timer = 0;
this.starDuration = 0;
// 生成一个新的星星
this.spawnNewStar();
// 初始化计分
this.score = 0;
},
然后在spawnNewStar方法最后加入重置计时器的逻辑,其中this.minStarDuration和this.maxStarDuration是我们一开始声明的Game组件属性,用来规定星星消失时间的随机范围:

// Game.js
spawnNewStar: function() {
// …
// 重置计时器,根据消失时间范围随机取一个值
this.starDuration = this.minStarDuration + cc.random0To1() * (this.maxStarDuration – this.minStarDuration);
this.timer = 0;
},
在update方法中加入计时器更新和判断超过时限的逻辑:

// Game.js
update: function (dt) {
// 每帧更新计时器,超过限度还没有生成新的星星
// 就会调用游戏失败逻辑
if (this.timer > this.starDuration) {
this.gameOver();
return;
}
this.timer += dt;
},
最后加入gameOver方法,游戏失败时重新加载场景。

// Game.js
gameOver: function () {
this.player.stopAllActions(); //停止 player 节点的跳跃动作
cc.director.loadScene(‘game’);
}
对Game脚本的修改就完成了,保存脚本,然后打开Star脚本,我们需要为即将消失的星星加入简单的视觉提示效果,在update方法最后加入以下代码:

// Star.js
update: function() {
// …
// 根据 Game 脚本中的计时器更新星星的透明度
var opacityRatio = 1 – this.game.timer/this.game.starDuration;
var minOpacity = 50;
this.node.opacity = minOpacity + Math.floor(opacityRatio * (255 – minOpacity));
}
保存Star脚本,我们的游戏玩法逻辑就全部完成了!现在点击预览游戏按钮,我们在浏览器看到的就是一个有核心玩法、激励机制、失败机制的合格游戏了。

加入音效
尽管很多人玩手游的时候会无视声音,我们为了教程展示的工作流程尽量完整,还是要补全加入音效的任务。

跳跃音效
首先加入跳跃音效,打开Player脚本,添加引用声音文件资源的jumpAudio属性:

// Player.js
properties: {
// …
// 跳跃音效资源
jumpAudio: {
default: null,
url: cc.AudioClip
},
},
然后改写setJumpAction方法,插入播放音效的回调,并通过添加playJumpSound方法来播放声音:

// Player.js
setJumpAction: function () {
// 跳跃上升
var jumpUp = cc.moveBy(this.jumpDuration, cc.p(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
// 下落
var jumpDown = cc.moveBy(this.jumpDuration, cc.p(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
// 添加一个回调函数,用于在动作结束时调用我们定义的其他方法
var callback = cc.callFunc(this.playJumpSound, this);
// 不断重复,而且每次完成落地动作后调用回调来播放声音
return cc.repeatForever(cc.sequence(jumpUp, jumpDown, callback));
},

playJumpSound: function () {
    // 调用声音引擎播放声音
    cc.audioEngine.playEffect(this.jumpAudio, false);
},

得分音效
保存Player脚本以后打开Game脚本,来添加得分音效,首先仍然是在properties中添加一个属性来引用声音文件资源:

// Game.js
properties: {
// …
// 得分音效资源
scoreAudio: {
default: null,
url: cc.AudioClip
}
},
然后在gainScore方法里插入播放声音的代码:

// Game.js
gainScore: function () {
this.score += 1;
// 更新 scoreDisplay Label 的文字
this.scoreDisplay.string = ‘Score: ‘ + this.score.toString();
// 播放得分音效
cc.audioEngine.playEffect(this.scoreAudio, false);
},
保存脚本,回到 层级编辑器 ,选中Player节点,然后从 资源管理器 里拖拽assets/audio/jump资源到Player组件的Jump Audio属性上。

然后选中Canvas节点,把assets/audio/score资源拖拽到Game组件的Score Audio属性上。

这样就大功告成了!完成形态的场景层级和各个关键组件的属性如下:

现在我们可以尽情享受刚制作完成的游戏了,您能打到多少分呢?别忘了您可以随时修改Player和Game组件里的移动控制和星星持续时间等游戏参数,来快速调节游戏的难度。修改组件属性之后需要保存场景,修改后的数值才会被记录下来。

到这里都是官网上面所写的。

下面进入二次开发。

官网上提供的这个游戏只有一个场景。那我就想是不是应该有一个开始场景还有一个结束场景,在结束场景中是不是应该有这次游戏的一个分数和一个重生玩一次的按键。

接下来我们就开始把这二个场景加入进来。

1.把官网上的完整代码下载下来。地址:https://github.com/cocos-creator/tutorial-first-game/releases/download/v1.2/complete_project.zip

2.导入到cocos creator。

3.打开导入的项目。现在我们开始进行二次开发。

我们创建二个场景:start、end

双击start场景,给start场景添加background 和 ground二张背景图片,和一个button,

在scripts下面新建start、end。

start文件

cc.Class({
extends: cc.Component,

properties: {

},

// LIFE-CYCLE CALLBACKS:

// onLoad () {},
btnClick:function(){
    console.log("btnClick");
    cc.director.loadScene("game");
},
//start () {},

// update (dt) {},

});

end文件

cc.Class({
extends: cc.Component,

properties: {
    // 暂存 Game 对象的引用
    // score label 的引用
    scores: {
        default: null,
        type: cc.Label
    },

},

// LIFE-CYCLE CALLBACKS:

onLoad () {
   // cc.log(Global.scorezf)
  this.scores.string  = 'gameOver Score:'+ Global.scorezf.toString();
},
btnClick:function(){
    // cc.log(this.game.score.toString())
    cc.director.loadScene('game');
},
//start () {},

// update (dt) {},

});

2.修改控制主角方向的事件。

打开Player文件,修改setInputControl方法

setInputControl: function () {
var self = this;
//add keyboard input listener to jump, turnLeft and turnRight
cc.eventManager.addListener({
// event: cc.EventListener.KEYBOARD,
event: cc.EventListener.TOUCH_ONE_BY_ONE,
// swallowTouches: false, // 设置是否吞没事件,在 onTouchBegan 方法返回 true 时吞掉事件,不再向下传递。
onTouchBegan: function (touch, event) { //实现 onTouchBegan 事件处理回调函数
// var target = event.getCurrentTarget(); // 获取事件所绑定的 target, 通常是cc.Node及其子类
//
// // 获取当前触摸点相对于按钮所在的坐标
// var locationInNode = target.convertToNodeSpace(touch.getLocation());
var canvas = cc.find(‘Canvas’);
var locationInNode = canvas.convertToNodeSpace(touch.getLocation());
cc.log(cc.director.getWinSize().width);
if(cc.director.getWinSize().width/2>locationInNode.x){
self.accLeft = true;
self.accRight = false;
this.xSpeed=0;
}else{
self.accLeft = false;
self.accRight = true;
this.xSpeed=0;
}
return true;
// var s = target.getContentSize();
// var rect = cc.rect(0, 0, s.width, s.height);

            //  if (cc.rectContainsPoint(rect, locationInNode)) {        // 判断触摸点是否在按钮范围内
        //    cc.log("sprite began... x = " + locationInNode.x + ", y = " + locationInNode.y);
            //    target.opacity = 180;
            //      return true;
            // }
            //   return false;
        },
        onTouchMoved: function (touch, event) {
            cc.log('Touch Moved: ' + event);
        },
        onTouchEnded: function (touch, event) {
            cc.log('Touch Ended: ' + event);
            var canvas = cc.find('Canvas');
            var locationInNode = canvas.convertToNodeSpace(touch.getLocation());

          //  cc.log(cc.director.getWinSize());
            if(cc.director.getWinSize().width/2>locationInNode.x){
                self.accLeft = false;
                self.accRight = false;
            }else{
                self.accLeft = false;
                self.accRight = false;
            }
        },
        onTouchCancelled: function (touch, event) {
            cc.log('Touch Cancelled: ' + event);
            // var target = event.getCurrentTarget();    // 获取事件所绑定的 target, 通常是cc.Node及其子类
            //
            // // 获取当前触摸点相对于按钮所在的坐标
            // var locationInNode = target.convertToNodeSpace(touch.getLocation());
            //
            // cc.log(cc.director.getWinSize());
            // if(cc.director.getWinSize().width/2>locationInNode.x){
            //     self.accLeft = false;
            //     self.accRight = false;
            // }else{
            //     self.accLeft = false;
            //     self.accRight = false;
            // }
        }





        // set a flag when key pressed
        // onKeyPressed: function(keyCode, event) {
        //     switch(keyCode) {
        //         case cc.KEY.a:
        //             self.accLeft = true;
        //             self.accRight = false;
        //             break;
        //         case cc.KEY.d:
        //             self.accLeft = false;
        //             self.accRight = true;
        //             break;
        //     }
        // },
        // // unset a flag when key released
        // onKeyReleased: function(keyCode, event) {
        //     switch(keyCode) {
        //         case cc.KEY.a:
        //             self.accLeft = false;
        //             break;
        //         case cc.KEY.d:
        //             self.accRight = false;
        //             break;
        //     }
        // }
    }, self.node);
    //cc.Node.EventType.TOUCH_MOVE

},

这个主要就不会跳屏幕外面去了。

最后构建发布时把初始场景设置为start.fire

下面是游戏运行效果:

————————————————
版权声明:本文为CSDN博主「qq_22109175」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_22109175/article/details/80569911