微信小游戏笔记
微信小游戏笔记
——基于傅猿猿老师的微信小游戏入门到实战课程
ES5和ES6对象的创建和继承的对比
- ES5:
- 函数声明的方法:
第一种方法:函数表达式
var fun1=function(){
console.log('this is a function');
}
第二种方法:直接声明,不推荐使用这种写法,因为函数声明会将function的作用域提升到所有变量之前,在加载js文件之后就先加载了函数
function fun1(){
console.log('this is a function');
}
- 类的继承:寄生组合继承
(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();
运行结果:
课前准备
- 学习前置条件
- 有简单的前端知识
- 对js有一定的认识
- 下载IDE
- 会查看官方文档
- 小游戏开发与调试环境的搭建
- 下载官方开发工具
- 建议下载编码体验更友好的IDE,比如WebStorm
- 为了后续学习,需要安装node和babel等工具链
- 配置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,点击确定即可
关键字
- 小游戏逻辑梳理
模块分解
- 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:重新开始按钮类
- 小游戏呈现原理解读
- 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();
}
});
}
}