├── game.js ├── logo.png ├── images ├── lqd.jpg ├── destroy.png ├── perfect.png └── restart.png ├── audio ├── destroy.wav └── restart.wav ├── game.json ├── screenshot ├── TIM截图20191015112133.png └── TIM截图20191015112145.png ├── js ├── framework │ ├── TouchEvent.js │ └── View.js ├── Utils.js ├── components │ ├── Background.js │ ├── AD.js │ ├── Image.js │ ├── ScrollBarText.js │ └── Bubble.js ├── State.js ├── Audio.js └── main.js ├── README.md └── project.config.json /game.js: -------------------------------------------------------------------------------- 1 | 2 | import Main from './js/main' 3 | 4 | new Main() 5 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/logo.png -------------------------------------------------------------------------------- /images/lqd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/images/lqd.jpg -------------------------------------------------------------------------------- /audio/destroy.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/audio/destroy.wav -------------------------------------------------------------------------------- /audio/restart.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/audio/restart.wav -------------------------------------------------------------------------------- /game.json: -------------------------------------------------------------------------------- 1 | { 2 | "deviceOrientation": "portrait", 3 | "showStatusBar":true 4 | } 5 | -------------------------------------------------------------------------------- /images/destroy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/images/destroy.png -------------------------------------------------------------------------------- /images/perfect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/images/perfect.png -------------------------------------------------------------------------------- /images/restart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/images/restart.png -------------------------------------------------------------------------------- /screenshot/TIM截图20191015112133.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/screenshot/TIM截图20191015112133.png -------------------------------------------------------------------------------- /screenshot/TIM截图20191015112145.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ikimiler/wx-niepaopao/HEAD/screenshot/TIM截图20191015112145.png -------------------------------------------------------------------------------- /js/framework/TouchEvent.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /** 4 | * 手势事件接口 5 | */ 6 | export default class TouchEvent{ 7 | 8 | constructor(){ 9 | 10 | } 11 | 12 | /** 13 | * 是否拦截手势事件 14 | */ 15 | interceptorTouchEvent(e){ 16 | return false; 17 | } 18 | } -------------------------------------------------------------------------------- /js/Utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 屏幕宽高 3 | */ 4 | var DeviceInfo = { 5 | windowWidth: 375, 6 | windowHeight: 667, 7 | }; 8 | 9 | try { 10 | let info = wx.getSystemInfoSync() 11 | DeviceInfo = info; 12 | } catch (e) { 13 | console.log('netlog-getSystemInfoSync-error', e) 14 | } 15 | 16 | export default DeviceInfo; -------------------------------------------------------------------------------- /js/components/Background.js: -------------------------------------------------------------------------------- 1 | import View from '../framework/View.js' 2 | 3 | /** 4 | * 背景组件 5 | */ 6 | export default class Background extends View { 7 | 8 | constructor(color,x,y,width,height) { 9 | super(x, y, width, height) 10 | this.color = color; 11 | } 12 | 13 | draw(cxt){ 14 | cxt.fillStyle = this.color; 15 | cxt.fillRect(this.x,this.y,this.width,this.height); 16 | } 17 | } -------------------------------------------------------------------------------- /js/State.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局数据存储 3 | */ 4 | class State { 5 | 6 | views = []; 7 | 8 | /** 9 | * 单个添加 10 | */ 11 | addView(view){ 12 | this.views.push(view) 13 | } 14 | 15 | /** 16 | * 批量添加 17 | */ 18 | concatViews(views){ 19 | this.views.push(...views) 20 | } 21 | 22 | } 23 | 24 | /** 25 | * 全局单利 26 | */ 27 | const instance = new State(); 28 | 29 | export default instance; -------------------------------------------------------------------------------- /js/components/AD.js: -------------------------------------------------------------------------------- 1 | import View from '../framework/View.js' 2 | import DeviceInfo from '../Utils.js' 3 | 4 | const width = DeviceInfo.windowWidth - 100; 5 | const height = width; 6 | 7 | const x = 50; 8 | const y = (DeviceInfo.windowHeight - height) / 2; 9 | 10 | /** 11 | * 随机弹出广告 12 | */ 13 | export default class AD extends View{ 14 | 15 | constructor(){ 16 | super("images/lqd.jpg", x, y, width, height) 17 | } 18 | 19 | draw(cxt){ 20 | cxt.drawImage(this.image,x,y,width,height) 21 | } 22 | } -------------------------------------------------------------------------------- /js/Audio.js: -------------------------------------------------------------------------------- 1 | 2 | function createAudio(src){ 3 | const audio = wx.createInnerAudioContext() 4 | audio.src = src; 5 | return audio; 6 | } 7 | 8 | /** 9 | * 泡泡音频 10 | */ 11 | const bubbleAudio = createAudio("audio/destroy.wav") 12 | 13 | bubbleAudio.bubble = function(){ 14 | bubbleAudio.stop(); 15 | bubbleAudio.play(); 16 | wx.vibrateShort() 17 | } 18 | 19 | /** 20 | * 开始音频 21 | */ 22 | const restartAudio = createAudio("audio/restart.wav") 23 | 24 | export { 25 | bubbleAudio, 26 | restartAudio 27 | } 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 微信小游戏-捏泡泡 2 | 3 | 主要用途:休闲娱乐减压利器 4 | 5 | 技术要点: 6 | 7 | * 纯canvas绘制,没有使用到游戏引擎 8 | * 简单实现一套事件机制,参考Android原生事件流程 9 | 10 | ![https://raw.githubusercontent.com/ikimiler/wx-niepaopao/master/screenshot/TIM%E6%88%AA%E5%9B%BE20191015112133.png](https://raw.githubusercontent.com/ikimiler/wx-niepaopao/master/screenshot/TIM%E6%88%AA%E5%9B%BE20191015112133.png) 11 | 12 | ![https://raw.githubusercontent.com/ikimiler/wx-niepaopao/master/screenshot/TIM%E6%88%AA%E5%9B%BE20191015112145.png](https://raw.githubusercontent.com/ikimiler/wx-niepaopao/master/screenshot/TIM%E6%88%AA%E5%9B%BE20191015112145.png) 13 | 14 | Tips: 15 | 使用微信开发者工具-小游戏-导入该项目即可运行。 -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目配置文件。", 3 | "setting": { 4 | "urlCheck": false, 5 | "es6": true, 6 | "postcss": true, 7 | "minified": true, 8 | "newFeature": true 9 | }, 10 | "compileType": "game", 11 | "libVersion": "1.9.94", 12 | "appid": "wxe8be453651eda17a", 13 | "projectname": "xyx", 14 | "simulatorType": "wechat", 15 | "simulatorPluginLibVersion": {}, 16 | "condition": { 17 | "search": { 18 | "current": -1, 19 | "list": [] 20 | }, 21 | "conversation": { 22 | "current": -1, 23 | "list": [] 24 | }, 25 | "game": { 26 | "currentL": -1, 27 | "list": [] 28 | }, 29 | "miniprogram": { 30 | "current": -1, 31 | "list": [] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /js/components/Image.js: -------------------------------------------------------------------------------- 1 | import View from '../framework/View.js' 2 | 3 | /** 4 | * 图片缓存,优化内存 5 | */ 6 | const imageCache = new Map(); 7 | 8 | export function addImageToCache(src,image){ 9 | imageCache.set(src,image) 10 | } 11 | 12 | /** 13 | * 图片组件 14 | */ 15 | export default class Image extends View{ 16 | 17 | constructor(src,x,y,width,height){ 18 | super(x,y,width,height) 19 | if (imageCache.has(src)){ 20 | this.image = imageCache.get(src) 21 | }else{ 22 | let image = wx.createImage(); 23 | image.src = src; 24 | this.image = image; 25 | imageCache.set(src,image); 26 | } 27 | } 28 | 29 | draw(cxt){ 30 | cxt.drawImage(this.image,this.x,this.y,this.width,this.height); 31 | } 32 | } -------------------------------------------------------------------------------- /js/components/ScrollBarText.js: -------------------------------------------------------------------------------- 1 | import View from '../framework/View.js' 2 | import DeviceInfo from '../Utils.js' 3 | 4 | const text = "健康游戏,愉快生活。"; 5 | const height = 75; 6 | 7 | export default class ScrollBarText extends View{ 8 | 9 | constructor(color){ 10 | super(0, height, DeviceInfo.windowWidth, height / 2) 11 | this.color = color; 12 | this.offsetx = 0; 13 | } 14 | 15 | draw(cxt){ 16 | if(Math.abs(this.offsetx) >= this.width * 2){ 17 | this.offsetx = 0; 18 | }else{ 19 | this.offsetx ++; 20 | } 21 | cxt.fillStyle = this.color; 22 | cxt.fillRect(this.x, this.y, this.width, this.height); 23 | cxt.font = 'bold 15px cursive' 24 | cxt.fillStyle = "white"; 25 | cxt.fillText(text, this.width - this.offsetx, this.y + this.y / 3,this.width) 26 | } 27 | } -------------------------------------------------------------------------------- /js/components/Bubble.js: -------------------------------------------------------------------------------- 1 | import Image, { 2 | addImageToCache 3 | } from './Image.js' 4 | 5 | 6 | const broken_image = wx.createImage(); 7 | broken_image.src = "images/destroy.png"; 8 | 9 | const perfect_image = wx.createImage(); 10 | perfect_image.src = "images/perfect.png"; 11 | 12 | /** 13 | * 图片缓存起来 14 | */ 15 | addImageToCache(broken_image.src, broken_image) 16 | addImageToCache(perfect_image.src, perfect_image) 17 | 18 | const image_width = 40; 19 | 20 | /** 21 | * 气泡 22 | */ 23 | export default class Bubble extends Image { 24 | 25 | constructor(broken = false, x, y) { 26 | super(broken ? broken_image.src : perfect_image.src, x, y, image_width, image_width) 27 | this.broken = broken; 28 | } 29 | 30 | draw(cxt) { 31 | this.image = this.broken ? broken_image : perfect_image; 32 | super.draw(cxt); 33 | } 34 | } -------------------------------------------------------------------------------- /js/framework/View.js: -------------------------------------------------------------------------------- 1 | import TouchEvent from './TouchEvent.js' 2 | 3 | /** 4 | * 物体描述接口 5 | */ 6 | export default class View extends TouchEvent { 7 | 8 | /** 9 | * 每个物体所具备的基本属性 10 | */ 11 | constructor(x, y, width, height) { 12 | super(); 13 | this.x = x; 14 | this.y = y; 15 | this.width = width; 16 | this.height = height; 17 | 18 | this.rect = { 19 | left:x, 20 | top:y, 21 | right:x + width, 22 | bottom:y + height, 23 | } 24 | } 25 | 26 | /** 27 | * 开始绘制 28 | */ 29 | draw(cxt) { 30 | 31 | } 32 | 33 | interceptorTouchEvent(e) { 34 | let targetX = e.touches[0].clientX 35 | let targetY = e.touches[0].clientY 36 | let touchable = targetX >= this.rect.left && targetX <= this.rect.right && targetY >= this.rect.top && targetY <= this.rect.bottom; 37 | if (touchable) { 38 | //回调点击事件 39 | this.listener && this.listener(this) 40 | return true; 41 | } 42 | return false; 43 | } 44 | 45 | /** 46 | * 设置点击事件回调 47 | */ 48 | setOnclickListener(listener) { 49 | this.listener = listener; 50 | } 51 | 52 | /** 53 | * view的唯一标识 54 | */ 55 | toStringID(){ 56 | return JSON.stringify(this.rect) 57 | } 58 | } -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import DeviceInfo from './Utils.js' 2 | import Image from './components/Image.js' 3 | import Background from './components/Background.js' 4 | import ScrollBarText from './components/ScrollBarText.js' 5 | import Bubble from './components/Bubble.js' 6 | import AD from './components/AD.js' 7 | import State from './State.js' 8 | import { 9 | bubbleAudio, 10 | restartAudio 11 | } from './Audio.js' 12 | 13 | 14 | const image_width = 40; 15 | 16 | /** 17 | * 初始化泡泡 18 | */ 19 | function configBubble() { 20 | 21 | /** 22 | * 横向默认以8为基准,间距动态计算 23 | * 竖向根据得到的间距动态计算个数 24 | */ 25 | let row_count = 8, 26 | column_count = 0; 27 | 28 | /** 29 | * 公式:屏幕宽度减掉8个图片的宽度总和除以图片个数+1得到均匀的间距 30 | */ 31 | let space = Math.floor((DeviceInfo.windowWidth - image_width * row_count) / (row_count + 1)); 32 | 33 | /** 34 | * 开始计算竖向的可绘制的个数 35 | */ 36 | column_count = Math.floor(DeviceInfo.windowHeight / (image_width + space)) 37 | 38 | /** 39 | * x的默认起始位置 40 | */ 41 | let offsetx = 0; 42 | 43 | /** 44 | * y的起始位置需要重新计算以保证上下距离屏幕间距均匀 45 | */ 46 | let offsety = Math.floor((DeviceInfo.windowHeight - column_count * (image_width + space)) / 2); 47 | 48 | let bubbles = []; 49 | /** 50 | * 双层for循环开始绘制气泡 51 | */ 52 | for (let i = 0; i < column_count; i++) { 53 | let start_y = i * image_width + (i + 1) * space + offsety; 54 | for (let j = 0; j < row_count; j++) { 55 | let start_x = j * image_width + (j + 1) * space + offsetx; 56 | let bubble = new Bubble(false, start_x, start_y) 57 | //设置点击事件 58 | bubble.setOnclickListener(function(view) { 59 | if (view.broken) return; 60 | view.broken = true; 61 | //播放音效以及震动 62 | bubbleAudio.bubble(); 63 | //随机 64 | randomADS(); 65 | }) 66 | bubbles.push(bubble) 67 | } 68 | } 69 | return bubbles; 70 | } 71 | 72 | /** 73 | * 随机弹出广告 74 | */ 75 | function randomADS() { 76 | // State.addView(new AD()) 77 | } 78 | 79 | /** 80 | * 配置重玩按钮 81 | */ 82 | function configRestart() { 83 | let width = 200 / 1.5; 84 | let height = 100 / 1.6; 85 | let x = DeviceInfo.windowWidth / 2 - width / 2; 86 | let y = DeviceInfo.windowHeight - height - 20; 87 | let restart = new Image("images/restart.png", x, y, width, height) 88 | restart.setOnclickListener(function(view) { 89 | restartAudio.play(); 90 | let views = State.views; 91 | for(let i = 0; i < views.length; i ++){ 92 | let item = views[i] 93 | if (item instanceof Bubble && item.broken){ 94 | item.broken = false; 95 | } 96 | } 97 | }) 98 | return restart; 99 | } 100 | 101 | /** 102 | * 配置背景 103 | */ 104 | function configBackground() { 105 | return new Background("#90d7ec", 0, 0, DeviceInfo.windowWidth, DeviceInfo.windowHeight) 106 | } 107 | 108 | function configScrollBarText(){ 109 | return new ScrollBarText("red") 110 | } 111 | 112 | /** 113 | * 游戏主函数 114 | */ 115 | export default function Main() { 116 | //背景 117 | let bg = configBackground(); 118 | //气泡 119 | let bubbles = configBubble(); 120 | //重玩按钮 121 | let restart = configRestart(); 122 | //scrollbartext 123 | let scrollBarText = configScrollBarText(); 124 | 125 | State.addView(bg) 126 | State.concatViews(bubbles) 127 | State.addView(restart) 128 | State.addView(scrollBarText) 129 | 130 | //注册手势监听器 131 | initEventListener(); 132 | 133 | //looper循环 134 | this.loop = looper.bind(this) 135 | this.animationId = requestAnimationFrame(this.loop) 136 | } 137 | 138 | function looper() { 139 | startDraw() 140 | this.animationId = requestAnimationFrame(this.loop) 141 | } 142 | 143 | /** 144 | * 上层画布 145 | */ 146 | const canvas = wx.createCanvas() 147 | const context = canvas.getContext('2d') 148 | console.log('netlog-', context) 149 | 150 | /** 151 | * 开始绘制 152 | */ 153 | function startDraw() { 154 | for (let i = 0; i < State.views.length; i++) { 155 | let item = State.views[i]; 156 | item.draw(context); 157 | } 158 | } 159 | 160 | 161 | /** 162 | * 注册手势监听 163 | */ 164 | function initEventListener() { 165 | wx.onTouchStart(e => { 166 | let x = e.touches[0].clientX 167 | let y = e.touches[0].clientY 168 | //逆向迭代,手势事件优先最后绘制的内容 169 | let views = State.views; 170 | for (let i = views.length - 1; i >= 0; i--) { 171 | let view = views[i]; 172 | if (view.interceptorTouchEvent(e)) { 173 | break; 174 | } 175 | } 176 | }) 177 | } --------------------------------------------------------------------------------