微信小游戏笔记

微信小游戏笔记

——基于傅猿猿老师的微信小游戏入门到实战课程

ES5和ES6对象的创建和继承的对比

  • ES5:
  1. 函数声明的方法:
    第一种方法:函数表达式
var fun1=function(){
    console.log('this is a function');
}

第二种方法:直接声明,不推荐使用这种写法,因为函数声明会将function的作用域提升到所有变量之前,在加载js文件之后就先加载了函数

function fun1(){
    console.log('this is a function');
}
  1. 类的继承:寄生组合继承
(function () {
    'use strict';

    var Animal = function (name, age) {
        this.name = name;
        this.age = age;
    };

    Animal.prototype.say = function () {
        console.log(this.name + " " + this.age);
    };

    var cat = new Animal('小猫', 3);
    cat.say();

    var Cat = function (name, age) {
        Animal.apply(this, arguments);
    };

    //浅克隆,克隆一个新的对象
    Cat.prototype = Object.create(Animal.prototype);
    //直接将对象克隆进来
    //Cat.prototype=new Animal();
    Cat.prototype.say=function(){
        console.log('子类:name '+this.name+" age "+this.age);
    };
    var cat1=new Cat('子猫',5);
    cat1.say();

})();

运行结果:
微信小游戏笔记
3. call() apply()方法:调用一个对象的方法,用另一个对象替换当前对象

Animal.prototype.say.call(cat);
Animal.prototype.say.apply(cat);
var params = {
    name: '小猫2',
    age: 4
}
cat.say.call(params);

运行结果:
微信小游戏笔记

  • ES6:
    和java像似,h5中script不需要加type,ES6默认使用不用添加严格模式,所以在ES6中不用添加’use strict’
    推荐教程:阮一峰的ES6
class Animal {
    constructor(name = "无姓名", age = 0) {
        this.name = name;
        this.age = age;
    }

    say() {
        console.log(this.name + " " + this.age);
    }
}

class Cat extends Animal {
    constructor(name,age){
        super(name,age);
    }
}

const cat=new Cat('小猫哈哈',5);
cat.say();

运行结果:
微信小游戏笔记

课前准备

  1. 学习前置条件
  • 有简单的前端知识
  • 对js有一定的认识
  • 下载IDE
  • 会查看官方文档
  1. 小游戏开发与调试环境的搭建
  • 下载官方开发工具
  • 建议下载编码体验更友好的IDE,比如WebStorm
  • 为了后续学习,需要安装node和babel等工具链
  1. 配置WebStorm
  • 用webstorm打开项目
  • 将js版本改成ES6
    微信小游戏笔记
  • 安装node和babel
    安装babel的方法:
    在命令行输入:npm install -g cnpm --registry=https://registry.npm.taobao.org(这个是淘宝镜像)回车
    然后输入:cnpm install -g --save-dev babel-cli babel-preset-env 回车
  • 配置babel
    打开Settings–>Tools–>File Watchers
    点击“+”,添加babel,点击确定即可
    关键字
  1. 小游戏逻辑梳理
    模块分解
  • game.js:游戏全局的入口文件,是微信小游戏必须有的一个文件
  • Main.js:程序的主类,主要用来初始化canvas和和一些全局对象,各个精灵和绑定点击事件
  • Director.js:程序导演类,用来控制游戏的逻辑和精灵的创建与销毁,控制游戏主循环
  • DataStore.js:储存游戏需要长期保存的变量和需要定时销毁的变量(相当于中间件)
  • Resources.js:游戏资源的数组
  • ResourcesLoader.js:资源加载器,保证游戏是在图片加载完成后开始主循环
  • Sprite.js:游戏精灵的基类,背景,陆地,铅笔,小鸟等都是它的子类
  • Background.js:背景类,在这个类中绑定一个图片资源,作为背景
  • Land.js:陆地类,可以移动
  • UpPencil.js:上半部分铅笔类
  • DownPencil.js:下半部分铅笔类
  • Birds.js:小鸟类
  • Score.js:计分器类
  • StartButton.js:重新开始按钮类
  1. 小游戏呈现原理解读
  • canvas的渲染原理是一层一层的渲染,依次为背景层,铅笔层,陆地层,然后再画上小鸟和按钮

分模块讲解

项目结构
微信小游戏笔记

index.html

  • 调节宽度和高度,主要模拟手机屏幕的大小方便调试。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0">
    <meta http-equiv="x-ua-compatible" content="ie=edge">
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        html,body{
            width: 100%;
            height: 100%;
            overflow: hidden;
        }
    </style>
    <title>Title</title>
</head>
<body>
    <canvas id="game_canvas" width="375" height="667"></canvas>
<script type="module" src="game.js"></script>
</body>
</html>

overflow属性的用法:
微信小游戏笔记

game.js

  • 作为这个游戏的入口,用于启动游戏,注意再导入Main类的时候,如果不加.js的话低版本的浏览器会报错,而微信开发者工具不会,所以为了增强程序的兼容性,推荐加上.js
import {Main} from "./Main.js";
new Main();

js/base/Resources.js

  • 使用常量储存所有素材的路径信息
  • ES6有模块化的机制,如果外部要引用需要加入export关键字,可以在文件尾加入export {Resources},也可以在类开头加上export关键字
export const Resources=[
    ['background','res/background.png'],
    ['land','res/land.png'],
    ['pencilUp','res/pie_up.png'],
    ['pencilDown','res/pie_down.png'],
    ['birds','res/birds.png'],
    ['startButton','res/start_button.png']
];

js/base/ResourceLoader.js

  • 用于加载图片资源的类
  • var和let的区别:var是一个全局的声明,而let只在作用域中生效,循环中用let再好不过
//确保在图片资源完成后再进行渲染
import {Resources} from "./Resources.js";

export class ResourceLoader {

    //提前声明变量在浏览器中会报错,原因在于浏览器兼容性的问题,不是语法的问题
    //map=null;

    //构造器的作用在于将Resources中的路径转化为Image对象储存在map中
    constructor() {
        //Map类似于java中的Map,存储键值对,可传入数组进去
        this.map = new Map(Resources);
        for (let [key, value] of this.map) {
            //遍历map,分别将map中的key和values取出来
            //与微信小游戏中的Image创建方法不同,微信小游戏中的Image是用wx.createImage()来创建的
            const image = new Image();
            image.src = value;
            this.map.set(key, image);
        }
    }

    //加载结束是调用的方法
    onLoaded(callback) {
        let loadCount = 0;
        for (let value of this.map.values()) {
            value.onload = () => {
                //使用箭头函数可以使函数中的this表示的是当前对象,而不是当前函数
                loadCount++;
                if (loadCount >= this.map.size) {
                    //当资源加载完成使调用函数
                    callback(this.map);
                }
            }
        }
    }

    //静态工厂设计模式,static和java中的static用法类似
    static create(){
        return new ResourceLoader();
    }

}

js/base/DataStore.js

  • 变量缓存器,方便我们在不同的类中访问和修改变量
//单例模式
export class DataStore {

    static getInstance() {
        if (!DataStore.instance) {
            DataStore.instance = new DataStore();
        }
        return DataStore.instance;
    }

    constructor() {
        this.map = new Map();
    }

    put(key, value) {
        //如果传进来的是一个类,就实例化该类的对象
        //ES6的class是作为一个function存在的
        if (typeof value === 'function') {
            value = new value();
        }
        this.map.set(key, value);
        //返回当前对象可以进行链式操作
        return this;
    }

    get(key) {
        return this.map.get(key);
    }

    //游戏结束,将map置空
    destroy() {
        for (let value of this.map.values()) {
            value = null;
        }
    }
}

js/base/Sprite.js

  • 精灵的基类,负责初始化精灵加载的资源和大小以及位置
import {DataStore} from "./DataStore.js";
//微信开发者工具区分后缀.js的大小写,而在浏览器中不会报错

export class Sprite {
    //es6中可以给参数赋默认值
    constructor(image = null,//图片对象
                srcX = null,
                srcY = null,//要剪裁的x,y坐标
                srcW = null,
                srcH = null,//要剪裁的宽度和高度
                x = 0,
                y = 0,//图形资源在canvas的显示位置
                width = 0,
                height = 0//剪裁完成之后,要使用的大小
    ) {
        this.dataStore=DataStore.getInstance();
        this.ctx=this.dataStore.ctx;
        this.image=image;
        this.srcX=srcX;
        this.srcY=srcY;
        this.srcW=srcW;
        this.srcH=srcH;
        this.x=x;
        this.y=y;
        this.width=width;
        this.height=height;
    }

    //获得图片对象
    static getImage(key){
        return DataStore.getInstance().res.get(key);
    }

    //如果没有传入参数就使用默认参数,传入参数就将默认参数覆盖
    draw(image=this.image,
         srcX=this.srcX,
         srcY=this.srcY,
         srcW=this.srcW,
         srcH=this.srcH,
         x=this.x,
         y=this.y,
         width=this.width,
         height=this.height){
        this.ctx.drawImage(
            image,
            srcX,
            srcY,
            srcW,
            srcH,
            x,
            y,
            width,
            height
        );
    }
}

js/player/Background.js

  • 背景类
import {Sprite} from "../base/Sprite.js";

export class Background extends Sprite {
    constructor() {
        //因为super上面不能访问this,所以用一个静态方法来获得image对象
        const image = Sprite.getImage('background');
        super(image,
            0, 0,
            image.width, image.height,
            0, 0,
            window.innerWidth, window.innerHeight//使用的大小以window的大小作为参考
        );
    }

}

js/player/Land.js

  • 陆地类
import {Sprite} from "../base/Sprite.js";
import {Director} from "../runtime/Director.js";

export class Land extends Sprite {
    constructor() {
        const image = Sprite.getImage('land');
        super(image, 0, 0,
            image.width, image.height,
            0, window.innerHeight - image.height,//让图片的底部刚好处在canvas的底部
            image.width, image.height
        );
        //底边水平变化坐标
        this.landX = 0;
        //地板移动速度
        this.landSpeed = Director.getInstance().moveSpeed;
    }

    draw() {
        this.landX = this.landX + this.landSpeed;
        if (this.landX > (this.image.width - window.innerWidth)) {
            this.landX = 0;
        }
        super.draw(
            this.image,
            this.srcX,
            this.srcY,
            this.srcW,
            this.srcH,
            -this.landX,
            this.y,
            this.width,
            this.height
        );
    }
}

js/player/Pencil.js

  • 铅笔类的基类
import {Sprite} from "../base/Sprite.js";
import {Director} from "../runtime/Director.js";

export class Pencil extends Sprite {
    constructor(image, top) {
        super(image,
            0, 0,
            image.width, image.height,
            //刚好在右侧看不到的位置
            window.innerWidth, 0,
            image.width, image.height
        );
        this.top=top;
    }

    draw() {
        this.x = this.x - Director.getInstance().moveSpeed;
        super.draw(
            this.image,
            0,
            0,
            this.width,
            this.height,
            this.x,
            this.y,
            this.width,
            this.height
        );
    }
}

js/player/UpPencil.js

  • 上半部分铅笔
import {Sprite} from "../base/Sprite.js";
import {Pencil} from "./Pencil.js";

export class UpPencil extends Pencil{
    constructor(top) {
        const image = Sprite.getImage('pencilUp');
        super(image, top);
    }

    draw() {
        this.y = this.top - this.height;
        super.draw();
    }
}

js/player/DownPencil.js

  • 下半部分铅笔
import {Pencil} from "./Pencil.js";
import {Sprite} from "../base/Sprite.js";

export class DownPencil extends Pencil {
    constructor(top) {
        const image = Sprite.getImage('pencilDown');
        super(image, top);
    }

    draw() {
        let gap = window.innerHeight / 5;//两个铅笔之间的空隙
        this.y = this.top + gap;
        super.draw();
    }
}

js/player/Birds.js

  • 小鸟类
//循环的渲染三张图片
//其实只是循环渲染图片的三个部分
import {Sprite} from "../base/Sprite.js";

export class Birds extends Sprite {
    constructor() {
        const image = Sprite.getImage('birds');
        super(image, 0, 0, image.width, image.height,
            0, 0, image.width, image.height);

        //小鸟的三种状态需要一个数组去储存
        //小鸟的宽是34,上下边距是10,小鸟的所有边距是9,小鸟的高度是24

        this.clippingX = [
            9,
            9 + 34 + 18,
            9 + 34 + 18 + 34 + 18
        ];
        this.clippingY = [10, 10, 10];
        this.clippingWidth = [34, 34, 34];
        this.clippingHeight = [24, 24, 24];

        //小鸟的初始位置
        const birdX = window.innerWidth / 4;
        const birdY = window.innerHeight / 2;
        this.birdsX = [birdX, birdX, birdX];
        this.birdsY = [birdY, birdY, birdY];
        const birdWidth = 34;
        const birdHeight = 24;
        this.birdsWidth = [birdWidth, birdWidth, birdWidth];
        this.birdsHeight = [birdHeight, birdHeight, birdHeight];

        this.y = [birdY, birdY, birdY];
        this.index = 0;
        this.count = 0;
        this.time = 0;
    }

    draw() {
        //切换三只小鸟的速度
        const speed = 0.2;
        this.count = this.count + speed;
        if (this.index >= 2) {
            this.count = 0;
        }
        //减速器
        this.index = Math.floor(this.count);

        //小鸟的位移,模拟重力加速度
        const g = 0.98 / 2.4;
        //向上偏移量
        const offsetUp = 30;

        const offsetY = (g * this.time * (this.time - offsetUp)) / 2;
        for (let i = 0; i <= 2; i++) {
            this.birdsY[i] = this.y[i] + offsetY;
        }
        this.time++;

        super.draw(
            this.image,
            this.clippingX[this.index],
            this.clippingY[this.index],
            this.clippingWidth[this.index],
            this.clippingHeight[this.index],
            this.birdsX[this.index],
            this.birdsY[this.index],
            this.birdsWidth[this.index],
            this.birdsHeight[this.index]
        );
    }


}

js/player/Score.js

  • 计分器类
import {DataStore} from "../base/DataStore.js";

export class Score {
    constructor() {
        this.ctx = DataStore.getInstance().ctx;
        this.scoreNumder = 0;
        //因为canvas刷新很快,所以需要一个变量控制加分,只加一次
        this.isScore = true;
    }

    draw() {
        this.ctx.font = '25px Arial';
        this.ctx.fillStyle = '#ff79c8';
        this.ctx.fillText(
            this.scoreNumder,
            window.innerWidth / 2,
            window.innerHeight / 18,
            1000
        );
    }
}

js/player/StartButton.js

  • 开始按钮类
import {Sprite} from "../base/Sprite.js";

export class StartButton extends Sprite {
    constructor() {
        const image = Sprite.getImage('startButton');
        super(image,
            0, 0,
            image.width, image.height,
            (window.innerWidth - image.width) / 2,
            (window.innerHeight - image.height) / 2.5,
            image.width, image.height
        );
    }
}

js/runtime/Director.js

  • 导演类,控制游戏中所有逻辑
  • 由于在游戏中只需要一个导演类,所以将其设计为单例模式
import {DataStore} from "../base/DataStore.js";
import {UpPencil} from "../player/UpPencil.js";
import {DownPencil} from "../player/DownPencil.js";

export class Director {

    constructor() {
        this.dataStore = DataStore.getInstance();
        this.moveSpeed = 2;
        this.isGameover = false;
    }

    /**
     * 创建铅笔
     */
    createPencil() {
        //屏幕高度的八分之一和屏幕高度的二分之一看上去会比较顺眼
        const minTop = window.innerHeight / 8;
        const maxTop = window.innerWidth / 2;
        const top = minTop + Math.random() * (maxTop - minTop);
        this.dataStore.get('pencils').push(new UpPencil(top));
        this.dataStore.get('pencils').push(new DownPencil(top));
    }

    run() {
        this.check();
        if (!this.isGameover) {
            this.dataStore.get('background').draw();

            const pencils = this.dataStore.get('pencils');
            //销毁越界铅笔
            if (pencils[0].x + pencils[0].width <= 0 &&
                pencils.length === 4) {
                //刚好越过左侧边界并且铅笔的个数等于4
                //shift的功能是将数组的第一个元素推出数组,然后再把数组的个数减一
                pencils.shift();
                pencils.shift();
                this.dataStore.get('score').isScore = true;
            }
            if (pencils[0].x <= (window.innerWidth - pencils[0].width) / 2 &&
                pencils.length === 2) {
                this.createPencil();
            }

            //把铅笔放在地板之上初始化就可以达到先绘制铅笔,再绘制地板的效果
            this.dataStore.get('pencils').forEach(function (value, index, array) {
                //forEach有三个参数,(值,角标,数组)
                value.draw();
            });
            this.dataStore.get('land').draw();

            //画小鸟
            this.dataStore.get('birds').draw();

            let timer = requestAnimationFrame(() => this.run());
            this.dataStore.put('timer', timer);
        } else {
            console.log('游戏结束');
            this.dataStore.get('startButton').draw();
            //停止刷新
            cancelAnimationFrame(this.dataStore.get('timer'));
            //将所有精灵置空
            this.dataStore.destroy();
        }
        this.dataStore.get('score').draw();
    }

    //小鸟点击事件
    birdsEvent() {
        for (let i = 0; i <= 2; i++) {
            this.dataStore.get('birds').y[i] = this.dataStore.get('birds').birdsY[i];
        }
        this.dataStore.get('birds').time = 0;
    }

    //判断小鸟是否和铅笔撞击
    static isStrike(bird, pencil) {
        let s = false;
        if (bird.top > pencil.bottom ||
            bird.bottom < pencil.top ||
            bird.right < pencil.left ||
            bird.left > pencil.right
        ) {
            s = true;
        }
        return !s;
    }

    //判断小鸟是否撞击地板和铅笔
    check() {
        const birds = this.dataStore.get('birds');
        const land = this.dataStore.get('land');
        const pencils = this.dataStore.get('pencils');
        const score = this.dataStore.get('score');
        //地板撞击判断
        if (birds.birdsY[0] + birds.birdsHeight[0] >= land.y) {
            this.isGameover = true;
            return;
        }

        //小鸟的边框模型
        const birdsBorder = {
            top: birds.y[0],
            bottom: birds.birdsY[0] + birds.birdsHeight[0],
            left: birds.birdsX[0],
            right: birds.birdsX[0] + birds.birdsWidth[0]
        };

        const length = pencils.length;
        for (let i = 0; i < length; i++) {
            const pencil = pencils[i];
            const pencilBorder = {
                top: pencil.y,
                bottom: pencil.y + pencil.height,
                left: pencil.x,
                right: pencil.x + pencil.width
            };
            if (Director.isStrike(birdsBorder, pencilBorder)) {
                console.log('撞到铅笔了');
                this.isGameover = true;
                return;
            }
        }

        //加分逻辑
        if (birds.birdsX[0] > pencils[0].x + pencils[0].width
            && score.isScore) {
            score.isScore = false;
            score.scoreNumder++;
        }
    }

    static getInstance() {
        if (!Director.instance) {
            Director.instance = new Director();
        }
        return Director.instance;
    }
}

Main.js

  • 初始化整个游戏的精灵
import {ResourceLoader} from "./js/base/ResourceLoader.js";
import {Director} from "./js/runtime/Director.js";
import {Background} from "./js/player/Background.js";
import {DataStore} from "./js/base/DataStore.js";
import {Land} from "./js/player/Land.js";
import {Birds} from "./js/player/Birds.js";
import {StartButton} from "./js/player/StartButton.js";
import {Score} from "./js/player/Score.js";

export class Main {
    constructor() {
        this.canvas = document.getElementById("game_canvas");
        this.ctx = this.canvas.getContext('2d');
        this.dataStore = DataStore.getInstance();
        this.director = Director.getInstance();
        const loader = ResourceLoader.create();
        loader.onLoaded(map => this.onResourceFirstLoaded(map));
    }

    onResourceFirstLoaded(map) {
        //资源加载完毕之后,要给DataStore赋不变的值
        //不将ctx放入map中的原因是:当游戏结束之后需要销毁map中的变量,而不需要销毁ctx
        this.dataStore.ctx = this.ctx;
        this.dataStore.res = map;
        this.init();
    }

    //初始化
    init() {

        //判断是否游戏结束
        this.director.isGameover = false;

        this.dataStore
            .put('score',Score)
            .put('startButton', StartButton)
            .put('birds', Birds)
            .put('pencils', [])
            .put('background', Background)
            .put('land', Land);
        //创建铅笔要在游戏运行之前
        this.director.createPencil();
        this.director.run();

        this.registerEvent();
    }

    //注册事件
    registerEvent() {
        //箭头函数的好处在于函数中的this指向的是Main,而不是canvas
        this.canvas.addEventListener('touchstart', e => {
            //屏蔽js的事件冒泡
            e.preventDefault();
            if (this.director.isGameover) {
                console.log('游戏开始');
                this.init();
            } else {
                this.director.birdsEvent();
            }
        });
    }
}