17
头图

A small game to realize "Sheep and Sheep-Animal Edition"

I believe everyone has played the game "Sheep and Sheep", which has been popular in the past two days. So while playing this game, I think everyone will be curious about the implementation of this game. This article will show you how to use css, html, and js. To implement an animal version of the game.

First of all, I used 2 plugins. The first plugin is flexible.js . This plugin is to set the font size of the root element for different devices, which is an adaptation scheme for mobile terminals.

Because the rem layout is used here, which is adaptive for the mobile terminal, the rem layout scheme is chosen here.

There is also a pop-up box plug-in, which I implemented by myself a long time ago, which is popbox.js . Regarding this plug-in, this article does not intend to explain the implementation principle, only the usage principle:

 ewConfirm({
    title: "温馨提示", //弹框标题
    content: "游戏结束,别灰心,你能行的!", //弹框内容
    sureText: "重新开始", //确认按钮文本
    isClickModal:false, //点击遮罩层是否关闭弹框
    sure(context) {
        context.close();
        //点击确认按钮执行的逻辑
    },//点击确认的事件回调
})

After the introduction of this js, an ewConfirm method will be bound to the window object. This method passes in a custom object. The attributes of the object are title,content,sureText,cancelText,cancel,sure,isClickModal these attributes, of course, the cancel button is not used here. So don't go into details.

As the comments say, the meaning of each attribute will not be repeated here.

Then the html code is like this:

 <!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>

You can see that there is nothing in the html code, because the DOM elements inside are all dynamically generated in the js code, so the code here in script.js is the core, which will be discussed later, and then look at the style code, Also simpler.

 * {
    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;
}

The first is the wildcard selector '*' to match all elements, and set the style to initialize, then set the width and height of the html and body elements to 100%, and hide the overflowing content, then set a background image to the body element, and The body element adopts a flexible box layout, centered horizontally and vertically.

Next is the box element box that is eliminated in the middle. It is also very simple to set the positioning and fix the width and height to 8rem.

Next is box-item, which represents each block element, that is, each block element of Xiaoxiaole, and then there is a collection box element at the bottom of the sheep that stores the selected block element, that is, ew-collection, and then the left and right The card container element for the hierarchy is not visible.

The last thing is the shadow effect that is added to make the block elements look cascading.

The css core code is relatively simple, let's look at the javascript code next.

Before we start, we need to import the list of image materials, as follows:

 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'
]

Then call our encapsulated Game class when onload, that is, when the page is loaded, and pass this material list into it. as follows:

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

Next, let's look at the core code of the Game class, first define this class:

 class Game {
    constructor(originSource, bindElement){
        //核心代码
    }
}

This class name has 2 parameters, the first parameter is the material list, and the second parameter is the bound DOM element. By default, if it is not passed in, it will be bound to document.body. Therefore, we initialize some variables that need to be used later in the constructor. as follows:

 //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初始化方法

Here, the document object is stored, the original material list, and the bound dom element are stored, and then the source is also defined to store the scrambled material list, and the temp is used to store the clicked element, which is convenient for eliminating and adding shadows. these operations.

There are also four variables, which actually store dom elements, as described in the comments.

Next, the init method is to perform some initialization operations. This method returns a Promise so that the then method can be called, and then startHandler is the core logic function of the game start. This will be discussed later. Note that there is an interesting point here, that is bind( this), because this inside the then method does not refer to the instance of Game, so you need to call the bind method to modify the this binding. Next, let's see what the init method does.

 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();
    })
}

Obviously, this method returns a Promise as mentioned above, which defines the template template code, that is, the structure of the page, then calls the create method to create a container element, and inserts this element before the first child element of the body element, and then in the Insert the created page structure before the container element, delete the container element, and resolve it out, so as to add the page element to the body element. Two utility functions are involved here, let's take a look at them separately, as follows:

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

The create method actually calls the createElement method to create a DOM element. This.doc refers to the document file object, that is to say, the create method is just an encapsulation of document.createElement. Look at the createElement method.

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

The createElement method passes in 2 parameters, the first parameter is a DOM element, and the second parameter is a DOM element string, indicating that the incoming template element is inserted before the first DOM element. This method can refer to code-segment .

The init method is an implementation of dynamically creating elements, and the next is the implementation of the startHandler function.

 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 is the core implementation, so it is impossible to have only the above code, but we have to write a step-by-step split. The above code does two logics, getting DOM elements and resetting. A $ method is involved here, as follows:

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

The $ method passes in 2 parameters, the first parameter is a selector, which is a string, and the second parameter is a DOM element, which is actually a package of document.querySelector. Of course there is also a $$ method, similar, as follows:

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

Next is the resetHandler method, as follows:

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

It can be seen that the resetHandler method is indeed reset as it is defined. We want to reset the data used and the child nodes of the DOM element.

Let's go ahead and add this code after the startHandler aka resetHandler method:

 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);
    //后续还有逻辑
}

It can be seen that this is actually an operation of adding and converting the material data. The randomList method, as the name suggests, is to disrupt the order of the material list. Let's look at the source code of this utility method:

 /**
* 打乱顺序
* @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;
}

The function of this function is to randomly shuffle the material list for random purposes. Next, let's continue.

 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);
            }
        }
    }
}

The code here is very long, but to summarize it in two points, add block elements, and bind click events to each block element. We know that every block element that is eliminated will have a cascading effect, so we have to achieve the same effect here, how to achieve it?

The answer is positioning. We should know that positioning will be divided into hierarchical relationships. The higher the level, the higher it will be. The same principle is used here. The reason why 3 loops are used here is that the box elements are divided into 5 rows and 5 columns, so also That's why the loop is 5.

Then inside the loop, we just create each block element, each element is set with three attributes of x, y, z, and also added ew-box-${i}-${j}-${k} class name, obviously the x, y, The z attribute is associated with this class name, which facilitates our subsequent operations on the element.

The same each block element we also set the style, the class name is 'ew-box-item', the same each block element is also set to absolute positioning.

PS: You may be a little curious why I add a prefix of 'ew-' to each element. In fact, it is a prefix I like to add to the code I write, which means it is a sign of the code I wrote myself.

Next, take a single material from the material list, and the data structure taken out should be { src: 'image path', index: 'index value' }. Then set the background image for the element, which is the image path of the material list, the index attribute, and the left and top offset values. The reason why the left and top offset values are random here is because each block element has is random.

Next is clickHandler, which is the callback executed by clicking on the block element. We will not describe this in detail. Let's continue to look at it later, which is to add events to the element, use the addEventListener method, and add the block element to the box element.

Let's move on to the inside of the clickHandler function.

First of all, here is the judgment:

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

It's very simple, when we click on the element in our collection box, the click event cannot be triggered, so we need to make a judgment here.

Then there is another judgment. Those with shadow effects have an additional class name 'ew-shadow'. With shadow effects, it means that its level is the smallest, and it is covered by overlays, so it cannot be clicked.

Next, get the index value of the currently clicked block element, which is why an index attribute is set before adding the block element.

Then judge the number of clicks. If the click is the same, the index value of the click is stored in the temp object. Otherwise, the click is a different block element, and the index value is 1.

Then set the positioning of the element to static positioning, which is the default value, and add it to the collection box container element.

The next step is to reset the shadow effect and re-add the shadow effect. Here's a createShadow method, let's demystify it. as follows:

 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');
        }
    })
}

It is obvious here that the need to add shadows is determined by obtaining the class names set by the x, y, and z attributes, because the level of the element can be determined through the values of these three attributes. If it is not at the top, the element can be obtained, so just Add shadows. Note that the $$ method returns a NodeList collection, so you can get the length property.

The next step is to store the index value equal to 3, which means that 3 identical blocks are selected, then remove the three block elements from the collection box, and reset the corresponding index index value to 0.

The next for...in loop does of course count the block elements in the collection box. If it reaches 7, it means that the slot is full, then the game ends and the click event of the block element is removed. Let's look at the implementation of the game over method:

 gameOver() {
    const self = this;
    ewConfirm({
        title: "温馨提示",
        content: "游戏结束,别灰心,你能行的!",
        sureText: "重新开始",
        isClickModal:false,
        sure(context) {
            context.close();
            self.startHandler();
        }
    })
}

This is also the usage of the bullet box plug-in mentioned at the beginning. Calling the startHandler method in the callback of clicking confirm means restarting the game, which is nothing to say.

At this point, we have implemented the corresponding logic between each block element of the middle box element and the slot container element. Next, there are two points, that is, the set of block elements on both sides of the hierarchy that are covered and invisible. So continue to look at the subsequent logic of 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();
}

Obviously, the source material list is generally used to generate the corresponding left and right material lists. Similarly, the block element click event logic here should be very similar to the logic in the block container element, so there is nothing to say. We mainly look at the following code:

 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)
}

In fact, it is also well understood here, that is, the same block element is created. Here, according to index > len, it is determined whether to add to the right material container element or the left material element, and their top offsets should be the same, the main difference is the left And right only, the calculation method is also very simple.

Note that there is no need to set the x, y, z properties here, because there is no need to use the function to set the shadow.

So far, our little game of "Sheep and Sheep - Animal Edition" has been completed.

at last

If you are interested, you can refer to the source code .

Online example

ps: The code of this version of mine is really not very good, it is very rubbish. I recommend the fish of the fish skin boss.

Finally, thank you for watching. If you think this article is helpful to you, I hope you don't hesitate to like and subscribe, and click star with your little hands, hehe.


夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。