实现《羊了个羊-动物版》的小游戏

这两天火爆全场的《羊了个羊》游戏,置信大家都玩过了,那么在玩这个游戏的同时,我想大家都会好奇这个游戏的实现,本文就带大家应用css,html,js来实现一个动物版的游戏。

首先我用到了2个插件,第一个插件就是flexible.js,这个插件就是对不同设施设置根元素字体大小,也就是一个挪动端的适配计划。

因为这里应用了rem布局,针对挪动端做了自适应,所以这里抉择采纳rem布局计划。

还有一个弹框插件,我很早自行实现的,就是popbox.js,对于这个插件,本文不打算解说实现原理,只解说一下应用原理:

ewConfirm({    title: "舒适提醒", //弹框题目    content: "游戏完结,别灰心,你能行的!", //弹框内容    sureText: "从新开始", //确认按钮文本    isClickModal:false, //点击遮罩层是否敞开弹框    sure(context) {        context.close();        //点击确认按钮执行的逻辑    },//点击确认的事件回调})

引入了这个js之后,会在window对象上绑定一个ewConfirm办法,这个办法传入一个自定义对象,对象的属性有title,content,sureText,cancelText,cancel,sure,isClickModal这几个属性,当然这里没有用到cancel按钮,所以不细讲。

正如正文所说,每个属性代表的意思,这里不做赘述。

而后html代码是这样的:

<!DOCTYPE html><html lang="en"><head>    <meta charset="UTF-8">    <meta http-equiv="X-UA-Compatible" content="IE=edge">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>羊了个羊《动物版》</title>    <link rel="stylesheet" href="./style.css"></head><body></body><script src="https://www.eveningwater.com/static/plugin/popbox.min.js"></script><script src="https://www.eveningwater.com/test/demo/flexible.js"></script><script src="./script.js"></script></html>

能够看到html代码是什么都没有的,因为外面的DOM元素,咱们都放在js代码外面动静生成了,所以script.js这里的代码是外围,这个后续会讲到,接下来看款式代码,也比较简单。

* {    margin: 0;    padding: 0;    box-sizing: border-box;}body,html {    height: 100%;    width: 100%;    overflow: hidden;}body {    background: url('https://www.eveningwater.com/my-web-projects/js/21/img/2.gif') no-repeat center / cover;    display: flex;    justify-content: center;    align-items: center;}.ew-box {    position: absolute;    width: 8rem;    height: 8rem;}.ew-box-item {    width: 1.6rem;    height: 1.6rem;    border-radius: 4px;    border: 1px solid #535455;    background-position: center;    background-size: cover;    background-repeat: no-repeat;    cursor: pointer;    transition: all .4s cubic-bezier(0.075, 0.82, 0.165, 1);}.ew-collection {    width: 8rem;    height: 2.4rem;    display: flex;    align-items: center;    justify-content: center;    padding: 0 1rem;    background: url('https://www.eveningwater.com/static/dist/20d6c430c2496590f224.jpg') no-repeat center/cover;    position: fixed;    margin: auto;    overflow: auto;    bottom: 10px;}.ew-collection > .ew-box-item {    margin-right: 0.3rem;}.ew-left-source,.ew-right-source {    width: 2.6rem;    height: 1.2rem;    position: absolute;    top: 0;}.ew-left-source {    left: 0;}.ew-right-source {    right: 0;}.ew-shadow {    box-shadow: 0 0 50px 10px #535455 inset;}

首先是通配选择器'*'代表匹配所有的元素,并且设置款式初始化,而后是html和body元素设置宽高为100%,并且暗藏溢出的内容,而后给body元素设置了一个背景图,并且body元素采纳弹性盒子布局,程度垂直居中。

接下来是两头打消的盒子元素box,也很简略就是设置定位,和固定宽高为8rem。

接下来是box-item,代表每一个块元素,也就是消消乐的每一块元素,接着羊了个羊底部有一个存储选中块元素的收集盒子元素,也就是ew-collection,而后是左右的看不到层级的卡牌容器元素。

最初就是为了让块元素看起来有层叠成果而增加的暗影成果。

css外围代码也就比较简单,接下来咱们来看javascript代码。

在开始之前,咱们须要先导入图片素材列表,这里如下:

const globalImageList = [    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/1.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/2.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/3.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/4.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/5.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/6.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/7.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/8.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/9.jpg',    'https://www.eveningwater.com/my-web-projects/jQuery/7/img/10.jpg']

而后在onload也就是页面加载时调用咱们封装好的Game类,将这个素材列表传入其中。如下:

window.onload = () => {    const game = new Game(globalImageList);}

接下来,咱们来看Game类的外围代码吧,首先定义这个类:

class Game {    constructor(originSource, bindElement){        //外围代码    }}

这个类名有2个参数,第一个参数就是素材列表,第二个参数则是绑定的DOM元素,默认如果不传入的话,就绑定到document.body上。也因而,咱们在构造函数外面初始化一些后续须要用到的变量。如下:

//constructor外部this.doc = document;this.originSource = originSource;this.bindElement = bindElement || this.doc.body;// 存储随机打乱的元素this.source = [];// 存储点击的元素this.temp = {};// dom元素this.box = null; //存储消消乐块元素的容器盒子元素this.leftSource = null; //右边素材容器元素this.rightSource = null; //左边素材容器元素this.collection = null; //收集素材的容器元素// 须要调用bind办法批改this指向this.init().then(this.startHandler.bind(this)); //startHandler为游戏开始的外围逻辑函数,init初始化办法

这里存储了document对象,存储了原始素材列表,以及绑定的dom元素,而后还定义了source用来存储被打乱后的素材列表,以及temp用来存储点击的元素,不便做打消,增加暗影这些操作。

还有四个变量,其实也就是存储dom元素的,如正文所述。

接下来init办法就是做初始化的一些操作,这个办法返回一个Promise所以能力调用then办法,而后startHandler是游戏开始的外围逻辑函数,这个前面会讲到,留神这里有一个有意思的点,那就是bind(this),因为在then办法外部的this并不是指Game这个实例,所以须要调用bind办法批改this绑定,接下来咱们来看init办法做了什么。

init() {    return new Promise(resolve => {        const template = `<div class="ew-box" id="ew-box"></div>        <div class="ew-left-source" id="ew-left-source"></div>        <div class="ew-right-source" id="ew-right-source"></div>        <div class="ew-collection" id="ew-collection"></div>`;        const div = this.create('div');        this.bindElement.insertBefore(div, document.body.firstChild);        this.createElement(div, template);        div.remove();        resolve();    })}

很显然这个办法如前所述返回了一个Promise,外部定义了template模板代码,也就是页面的构造,而后调用create办法创立一个容器元素,并且向body元素的首个子元素之前插入这个元素,而后在这个容器元素之前插入创立好的页面构造,删除这个容器元素,并且resolve进来,从而达到将页面元素增加到body元素外部。这里波及到了两个工具函数,咱们别离来看看它们,如下:

create(name) {    return this.doc.createElement(name);}

create办法其实也就是调用createElement办法来创立一个DOM元素,this.doc指的就是document文件对象,也就是说,create办法只是document.createElement的一个封装而已。来看createElement办法。

createElement(el, str) {    return el.insertAdjacentHTML('beforebegin', str);}

createElement办法传入2个参数,第一个参数是一个DOM元素,第二个参数是一个DOM元素字符串,示意在第一个DOM元素之前插入传入的模板元素。这个办法能够参考code-segment。

init办法说白了就是动态创建元素的一个实现,接下来就是startHandler函数的实现。

startHandler() {    this.box = this.$('#ew-box');    this.leftSource = this.$('#ew-left-source');    this.rightSource = this.$('#ew-right-source');    this.collection = this.$('#ew-collection');    this.resetHandler();    //后续还有逻辑}

startHandler是外围实现,所以不可能只有下面那么点代码,然而咱们要写一步步的拆分,以上的代码就做了2个逻辑,获取DOM元素和重置。这里波及到了一个$办法,如下:

$(selector, el = this.doc) {    return el.querySelector(selector);}

$办法传入2个参数,第一个参数为选择器,是一个字符串,第二个参数为DOM元素,实际上就是document.querySelector的一个封装。当然还有一个$$办法,相似,如下:

$$(selector, el = this.doc) {    return el.querySelectorAll(selector);}

接下来是resetHandler办法,如下:

resetHandler() {    this.box.innerHTML = '';    this.leftSource.innerHTML = '';    this.rightSource.innerHTML = '';    this.collection.innerHTML = '';    this.temp = {};    this.source = [];}

能够看到resetHandler办法的确是如其定义的那样,就是做重置的,咱们要重置用到的数据以及DOM元素的子节点。

让咱们持续,在startHandler也就是resetHandler办法的前面,增加这样的代码:

startHandler() {    this.box = this.$('#ew-box');    this.leftSource = this.$('#ew-left-source');    this.rightSource = this.$('#ew-right-source');    this.collection = this.$('#ew-collection');    this.resetHandler();    for (let i = 0; i < 12; i++) {        this.originSource.forEach((src, index) => {            this.source.push({                src,                index            })        })    }    this.source = this.randomList(this.source);    //后续还有逻辑}

能够看到这里实际上就是对素材数据做了一个增加和转换操作,randomList办法顾名思义,就是打乱素材列表的程序。让咱们来看这个工具办法的源码:

/*** 打乱程序* @param {*} arr * @returns */randomList(arr) {    const newArr = [...arr];    for (let i = newArr.length - 1; i >= 0; i--) {        const index = Math.floor(Math.random() * i + 1);        const next = newArr[index];        newArr[index] = newArr[i];        newArr[i] = next;    }    return newArr;}

这个函数的作用就是将素材列表随机打乱以达到随机的目标,接下来,让咱们持续。

startHandler() {    this.box = this.$('#ew-box');    this.leftSource = this.$('#ew-left-source');    this.rightSource = this.$('#ew-right-source');    this.collection = this.$('#ew-collection');    this.resetHandler();    for (let i = 0; i < 12; i++) {        this.originSource.forEach((src, index) => {            this.source.push({                src,                index            })        })    }    this.source = this.randomList(this.source);    //后续还有逻辑    for (let k = 5; k > 0; k--) {        for (let i = 0; i < 5; i++) {            for (let j = 0; j < k; j++) {                const item = this.create('div');                item.setAttribute('x', i);                item.setAttribute('y', j);                item.setAttribute('z', k);                item.className = `ew-box-item ew-box-${i}-${j}-${k}`;                item.style.position = 'absolute';                const image = this.source.splice(0, 1);                // 1.44为item设置的宽度与高度                item.style.left = 1.44 * j + Math.random() * .1 * k + 'rem';                item.style.top = 1.44 * i + Math.random() * .1 * k + 'rem';                item.setAttribute('index', image[0].index);                item.style.backgroundImage = `url(${image[0].src})`;                const clickHandler = () => {                    // 如果是在收集框里是不可能点击的                    if(item.parentElement.className === 'ew-collection'){                        return;                    }                    // 没有暗影成果的元素才可能点击                    if (!item.classList.contains('ew-shadow')) {                        const currentIndex = item.getAttribute('index');                        if (this.temp[currentIndex]) {                            this.temp[currentIndex] += 1;                        } else {                            this.temp[currentIndex] = 1;                        }                        item.style.position = 'static';                        this.collection.appendChild(item);                        // 重置暗影成果                        this.$$('.ew-box-item',this.box).forEach(item => item.classList.remove('ew-shadow'));                        this.createShadow();                        // 等于3个就打消掉                        if (this.temp[currentIndex] === 3) {                            this.$$(`div[index="${currentIndex}"]`, this.collection).forEach(item => item.remove());                            this.temp[currentIndex] = 0;                        }                        let num = 0;                        for (let i in this.temp) {                            num += this.temp[i];                        }                        if (num > 7) {                            item.removeEventListener('click', clickHandler);                            this.gameOver();                        }                    }                }                item.addEventListener('click', clickHandler)                this.box.append(item);            }        }    }}

这里的代码很长,然而总结下来就二点,增加块元素,并为每个块元素绑定点击事件。咱们晓得羊了个羊每一个打消的块元素都会有层叠的成果,那么咱们这里也要实现同样的成果,如何实现呢?

答案就是定位,咱们应该晓得定位会分为层级关系,层级越高就会占下面,这里也是采纳同样的情理,这里之所以用3个循环,就是盒子元素是分成5行5列的,所以也就是为什么循环是5的起因。

而后在循环外部,咱们就是创立每一个块元素,每个元素都设置了x,y,z三个属性,并且还增加了ew-box-${i}-${j}-${k}类名,很显然这里的x,y,z属性和这个类名关联上了,这不便咱们后续对元素进行操作。

同样的每个块元素咱们也设置了款式,类名是'ew-box-item',同样的每个块元素也设置为相对定位。

PS: 大家可能有些好奇为什么每个元素我都加一个'ew-'的前缀,其实也就是我集体喜爱给本人写的代码加的一个前缀,代表这是我本人写的代码的一个标记。

接下来从素材列表中取出单个素材,取出的数据结构应该是{ src:'图片门路',index:'索引值' }这样。而后将该元素设置背景图,就是素材列表的图片门路,以及index属性,还有left和top偏移值,这里的left和top偏移值之所以是随机的,也就是因为每一个块元素都是随机的。

接下来是clickHandler也就是点击块元素执行的回调,这个咱们先不具体叙述,咱们持续往后看,就是为该元素增加事件,利用addEventListener办法,并且将块元素增加到box盒子元素中。

让咱们持续来看clickHandler函数外部。

首先这里有这样一个判断:

if(item.parentElement.className === 'ew-collection'){    return;}

很简略,当咱们的收集框外面点击该元素,是不能触发点击事件的,所以这里要做判断。

而后又是一个判断,有暗影成果的都是多了一个类名'ew-shadow',有暗影成果代表它的层级最小,被叠加遮盖住了,所以无奈被点击。

接下来获取以后点击块元素的index索引值,这也是为什么在增加块元素之前会设置一个index属性的起因。

而后判断点击的次数,如果点击的是同一个,则在temp对象外面存储点击的索引值,否则点击的是不同的块元素,索引值就是1。

而后将该元素的定位设置为动态定位,也就是默认值,并且增加到收集框容器元素当中去。

接下来就是重置暗影成果,并且从新增加暗影成果。这里有一个createShadow办法,让咱们来揭开它的神秘面纱。如下:

createShadow(){    this.$$('.ew-box-item',this.box).forEach((item,index) => {        let x = item.getAttribute('x'),            y = item.getAttribute('y'),            z = item.getAttribute('z'),            ele = this.$$(`.ew-box-${x}-${y}-${z - 1}`),            eleOther = this.$$(`.ew-box-${x + 1}-${y + 1}-${z - 1}`);        if (ele.length || eleOther.length) {            item.classList.add('ew-shadow');        }    })}

这里很显然通过获取x,y,z属性设置的类名来确定是否须要增加暗影,因为通过这三个属性值能够确定元素的层级,如果不是在最上方,就可能获取到该元素,所以就增加暗影。留神$$办法返回的是一个NodeList汇合,所以能够拿到length属性。

接下来就是通过存储的索引值等于3个,代表选中了3个雷同的块,那就要从收集框外面移除掉该三个块元素,并且重置对应的index索引值为0。

接下来的for...in循环所做的操作当然是统计收集框外面的块元素,如果达到了7个代表槽位满了,而后游戏完结,并且移除块元素的点击事件。咱们来看游戏完结这个办法的实现:

gameOver() {    const self = this;    ewConfirm({        title: "舒适提醒",        content: "游戏完结,别灰心,你能行的!",        sureText: "从新开始",        isClickModal:false,        sure(context) {            context.close();            self.startHandler();        }    })}

这也是最开始提到的弹框插件的用法,在点击确认的回调外面调用startHandler办法示意从新开始游戏,这没什么好说的。

到这里,咱们实现了两头盒子元素的每一个块元素与槽位容器元素的对应逻辑,接下来还有2点,那就是被遮蔽看不到层级的两边块元素汇合。所以持续看startHandler后续的逻辑。

startHandler() {    this.box = this.$('#ew-box');    this.leftSource = this.$('#ew-left-source');    this.rightSource = this.$('#ew-right-source');    this.collection = this.$('#ew-collection');    this.resetHandler();    for (let i = 0; i < 12; i++) {        this.originSource.forEach((src, index) => {            this.source.push({                src,                index            })        })    }    this.source = this.randomList(this.source);    for (let k = 5; k > 0; k--) {        for (let i = 0; i < 5; i++) {            for (let j = 0; j < k; j++) {                const item = this.create('div');                item.setAttribute('x', i);                item.setAttribute('y', j);                item.setAttribute('z', k);                item.className = `ew-box-item ew-box-${i}-${j}-${k}`;                item.style.position = 'absolute';                const image = this.source.splice(0, 1);                // 1.44为item设置的宽度与高度                item.style.left = 1.44 * j + Math.random() * .1 * k + 'rem';                item.style.top = 1.44 * i + Math.random() * .1 * k + 'rem';                item.setAttribute('index', image[0].index);                item.style.backgroundImage = `url(${image[0].src})`;                const clickHandler = () => {                    // 如果是在收集框里是不可能点击的                    if(item.parentElement.className === 'ew-collection'){                        return;                    }                    // 没有暗影成果的元素才可能点击                    if (!item.classList.contains('ew-shadow')) {                        const currentIndex = item.getAttribute('index');                        if (this.temp[currentIndex]) {                            this.temp[currentIndex] += 1;                        } else {                            this.temp[currentIndex] = 1;                        }                        item.style.position = 'static';                        this.collection.appendChild(item);                        // 重置暗影成果                        this.$$('.ew-box-item',this.box).forEach(item => item.classList.remove('ew-shadow'));                        this.createShadow();                        // 等于3个就打消掉                        if (this.temp[currentIndex] === 3) {                            this.$$(`div[index="${currentIndex}"]`, this.collection).forEach(item => item.remove());                            this.temp[currentIndex] = 0;                        }                        let num = 0;                        for (let i in this.temp) {                            num += this.temp[i];                        }                        if (num >= 7) {                            item.removeEventListener('click', clickHandler);                            this.gameOver();                        }                    }                }                item.addEventListener('click', clickHandler)                this.box.append(item);            }        }    }    //从这里开始剖析    let len = Math.ceil(this.source.length / 2);    this.source.forEach((item, index) => {        let div = this.create('div');        div.classList.add('ew-box-item')        div.setAttribute('index', item.index);        div.style.backgroundImage = `url(${item.src})`;        div.style.position = 'absolute';        div.style.top = 0;        if (index > len) {            div.style.right = `${(5 * (index - len)) / 100}rem`;            this.rightSource.appendChild(div);        } else {            div.style.left = `${(5 * index) / 100}rem`;            this.leftSource.appendChild(div)        }        const clickHandler = () => {            if(div.parentElement.className === 'ew-collection'){                return;            }            const currentIndex = div.getAttribute('index');            if (this.temp[currentIndex]) {                this.temp[currentIndex] += 1;            } else {                this.temp[currentIndex] = 1;            }            div.style.position = 'static';            this.collection.appendChild(div);            if (this.temp[currentIndex] === 3) {                this.$$(`div[index="${currentIndex}"]`, this.collection).forEach(item => item.remove());                this.temp[currentIndex] = 0;            }            let num = 0;            for (let i in this.temp) {                num += this.temp[i];            }            if (num >= 7) {                div.removeEventListener('click', clickHandler);                this.gameOver();            }        }        div.addEventListener('click', clickHandler);    });    this.createShadow();}

这里很显然取的是source素材列表的个别来别离生成对应的左右素材列表,同理,这外面的块元素点击事件逻辑应该是和块容器元素外面的逻辑是很类似的,所以没什么好说的。咱们次要看以下这段代码:

let div = this.create('div');div.classList.add('ew-box-item');div.setAttribute('index', item.index);div.style.backgroundImage = `url(${item.src})`;div.style.position = 'absolute';div.style.top = 0;if (index > len) {    div.style.right = `${(5 * (index - len)) / 100}rem`;    this.rightSource.appendChild(div);} else {    div.style.left = `${(5 * index) / 100}rem`;    this.leftSource.appendChild(div)}

其实这里也很好了解,也就是同样的创立块元素,这里依据index > len来确定是增加到左边素材容器元素还是右边素材元素,并且它们的top偏移量应该是统一的,次要不同在left和right而已,计算形式也很简略。

留神这里是不须要设置x,y,z属性的,因为不须要用到设置暗影的函数。

到此为止,咱们一个《羊了个羊——动物版》的小游戏就实现了。

最初

如有趣味能够参考源码。

在线示例

最初谢谢大家观看,如果感觉本文有帮忙到你,望不悭吝点赞和珍藏,动动小手点star,嘿嘿。