纯原生JS实现H5小游戏

试玩地址,请使用手机打开
代码放在了github

杀人游戏流程
2-1:冷启动界面
2-2:版本选择(其实只有一个版本)
2-3:设置玩家人数
3:身份查看页
4-1:法官查看页
4-2:显示天数
4-3:杀人页
4-4:黑夜解密
4-5:投票页
4-6:结果页
设置玩家人数,可满足6-18人游戏。设置完成之后,依次查看自己的身份,不要泄露给他人。然后法官查看身份,宣布开始游戏。
晚上,杀手杀人。白天公布被杀玩家,被杀的玩家发言,全体讨论后投票。
当水民全部死亡,杀手获得胜利,反之杀手全部死亡,水民获胜。

篇幅有限,这里我只说一下实现的最主要的地方

1. ​localStorage
关于localStorage与Cookie的与别:
Cookie主要用来保存登陆信息,大小被限制在4KB左右
localStorage即本地储存,是HTML5标准新加入的技术。大小通常为5M左右,兼容IE8及以上浏览器
用法:

  1. 写入:localStorage.str = "Hello world!"或者localStorage.setItem("str","Hello World!")
  2. 读取:var nStr = localStorage.str;或者var nStr = localStorage.getItem("str");
  3. localStorage是一个对象,所以有长度,可遍历
    localStorage注意事项:
    使用localStorage储存在本地均为字符串,比如
    1
    2
    localStorage.num = 3;
    console.log(typeof localStorage.num); //string

所以取本地储存中的值的时候,需要类型转换

1
2
var num = JSON.parse(localStorage.num);
console.log(typeof num); //number

对数组和对象要在储存前将其转换为JSON字符串才能保证取出来的还是原来的类型

1
2
3
4
var arr = [1,2,3];
localStorage.arr = JSON.stringify(arr); //转换为JSON字符串
var nStr = JSON.parse(localStorage.arr);
console.log(typeof nStr); //object

2. DOM操作
一些基本的DOM操作,比如这里向HTML追加元素

追加元素
采用的方法就是分别对杀手人数和水民人数进行遍历,进行以下操作

1
2
3
4
var parent = document.getElementById("xx"); //找到父元素
var span = document.creatElement("span");//穿件标签
span.innerHTML = "xxxxx";//标签内容
parent.appendChild(span);//向父元素追加

还有比如更改节点属性,在判断是否胜利的时候,如果达到胜利条件的话,就要更改a标签的href属性

1
element.setAttribute(name,value);

3. 计时器

计时器
主要思路是:
在每天开始的时候,使用new Date()获取当前的时间,然后存到localStorage里面,在每天结束的时候,再获取一个当前时间,并把localStorage里的时间取出来,相减,得到的就是这一天用的时间。
在每天开始时的代码

1
2
3
4
btn.addEventListener("click",function () {
var time = new Date();
localStorage.time = time;
})

每天结束时的代码

1
2
3
4
5
btn.addEventListener("click",function () {
var time = new Date() - new Date(localStorage.time);
var name = "time" + (deadPlayer.length / 2);//根据死亡了几个玩家来判断是第几天,分配localStorage的名字
localStorage.setItem(name,time);
})

结果就是这样的

储存的时间
还有一点

我们是在每天结束的时候相减得到该天的时间的,那如果这天只过了一半就获胜了呢?最后一天的时间就没了,所以在达到胜利条件的时候我们需要再储存一次最后一天的耗时

1
2
3
4
5
6
7
8
if(aliveWaters === 0 || aliveKillers === 0) {
//修复计时BUG
var lastTime = new Date() - new Date(localStorage.time);
localStorage.lastTime = lastTime;
btn.parentNode.setAttribute("href", "task4-6.html");
} else {
btn.parentNode.setAttribute("href", "task4-4.html")
}

如果活着的杀手或者活着的水民人数为0,就满足获胜条件,就立刻获取时间然后相减,因为不知道是第几天,所以就干脆命名为lastTime

我们得到了每天的时间,但是是存在本地储存里的,我们要取出来,之前说localStorage又长度,可遍历,这里就要对localStorage进行遍历。通过localStorage.getItem(name)来获取对应的值

因为我们在localStorage里存了很多东西,比如活着的人数,死亡玩家的数组等等,我们只想要nametime*这种格式的,所以我们验证name前4个字符为name的取出来存到数组里待用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//获取储存在本地的时间
var timeArr = [];
for(var i = 0; i < localStorage.length; i++) {
if(localStorage.key(i).substring(0,4) === "time") {
var getValue = localStorage.getItem(localStorage.key(i));
timeArr.push(Math.floor(parseInt(getValue) / 1000));
}
}
//因为存的还有time这个值,前面讲了它是用来获取当时的时间的,它的前4位也满足,所以也给取出来了,但是它是没用的,所以给他删除
timeArr.shift();
//修复计时bug
var lastTime = parseInt(localStorage.lastTime);
timeArr.push(Math.floor(lastTime / 1000));

然后关于时间的格式,经过上面的处理,我们得到了单位为秒的数组,但是我们不能直接把秒输出到HMTL里,我们应该输出为xx小时xx分钟xx秒这样的格式,还牵扯到补0的问题。

1
2
3
4
5
6
7
8
9
//把秒转化为标准时间
function time(s) {
var hours = Math.floor(s / 3600);
var minutes = Math.floor(s % 3600 / 60);
var seconds = Math.floor(s % 3600 % 60);
return (hours > 0 ? hours + "小时" + (minutes < 10 ? "0" : "") : "") + (minutes > 0 ? minutes + "分钟" +
(seconds < 10 && seconds > 0 ? "0" : ""): "") + (seconds > 0 ? seconds + "秒" : "");
}

关于return那一大块,其实就是一个条件操作符,相当于if(){}else{}
比如比较大小,如果num1>num2就把num1赋值给结果,否则就赋值num2。

1
var max = (num1 > num2) ? num1 : num2;

4. 点击事件
onclick 与 addEventListener的区别:
addEventListener是DOM2级事件处理程序,支持IE9以上,一个事件可以绑定多个函数,默认是false,冒泡阶段触发
onclick是DOM0级事件处理程序,冒泡阶段触发

比如一个使方框变色的函数可以这样写

1
2
3
4
5
for(var i = 0; i < liNum.length; i++) {
liNum[i].onclick = function () {
this.style.borderColor = "red";//注意这里要用this而不能用liNum[i],涉及到闭包,后面会讲
}
}

为各个方框添加了点击事件之后,点击变色之后,再点击另一个,问题出现了。第二个变色了,但是第一个并没有变回去。我们的本意并不是这样的。。
解决办法就是,先定义一个临时变量,让它指向一个没有用的DOM节点(不指定节点的话接下来会报错)

1
2
3
4
5
6
var temp = document.getElementById("fix");
btn.addEventListener("click",function(){
temp.style.borderColor = "#fff";
temp = this;
temp.style.borderColor = "red";
})

比如第一次我们点击了第一个li,此时temp等于id为fix的元素,使它边框变为白色,然后把第一个li赋值给temp,然后使temp(也就是第一个li)边框变红,然后第二次点击的时候,此时temp是等于第一个li的,我们会先把上次点击的li(也就是第一个)变回原来的颜色,然后把这次点击的li赋值给temp,继续变色。

这就完成了每次变色之前把上次点击的恢复颜色的效果,其实就是用一个中间值记录上次点击的数据

接下来讲刚刚说的闭包

比如我们有这样一个DOM结构

1
2
3
4
5
6
7
8
9
<div id="div1"></div>
<div id="div2"></div>
<div id="div3"></div>
<div id="div4"></div>
<div id="div5"></div>
<div id="div6"></div>
<div id="div7"></div>
<div id="div8"></div>
<div id="div9"></div>

和这样一段js代码

1
2
3
4
5
6
7
var count = document.getElementsByTagName("div");
for(var i = 0; i < count.length; i++) {
count[i].onclick = function () {
console.log(i);
}
}

结果并不是想象中那样每次点击输出对应的 i 。而是无论点击哪个,都会输出 9 。可以自己试一下

那为什么是这样呢。主要就是闭包+js没有块级作用域。每次迭代,就会形成一个闭包,onclick函数会有一个对 i 值的引用,但是,由于没有块作用域,所以这个i是全局作用域中的,也就是这几个函数所引用的i是在同一个作用域中,当然也就只有一个值

那么怎么解决呢? 有两种方法

第一种:使用ES的let语法

1
2
3
4
5
for(let i = 0; i < count.length; i++) {
count[i].onclick = function () {
console.log(i);
}
}

因为let会产生一个块级作用域,每次迭代都会产生一个块级作用域,里面有一个i,每个块级作用域互不影响。这样每次调用点击函数,就会访问对应作用域中的i,就好了

当然如果不想用ES6,我们还有第二种方法
第二种:使用立即执行函数

1
2
3
4
5
6
7
8
for(var i = 0; i < count.length; i++) {
(function (i) {
count[i].onclick = function(){
console.log(i);
}
})(i)
}

既然原因是没有块级作用域,那我们就手动模拟。每次迭代都会有一个立即执行函数,立即执行函数会创建一个函数作用域,我们把i作为参数传递进去,这样每个作用域也会保存一个i值

点击事件的最后
验证玩家身份和储存死亡玩家

杀手不能杀死杀死,不能杀死(投票给)已经死亡的玩家,这就需要我们在点击事件添加验证

我们在点击时,可以通过节点来获取到玩家身份

1
2
3
4
5
6
7
8
var deadNum = parseInt(this.lastChild.firstChild.nodeValue);
if(deadArr.indexOf(deadNum) >= 0) {
alert("该玩家已死")
} else if(this.firstChild.firstChild.nodeValue === "杀手") {
alert("杀手不能杀死杀手!")
} else {
...
}

记录死亡玩家:

点击确定按钮之后,该玩家被杀死,记录被杀的玩家编号,并push进死亡玩家数组。
同时检查死亡玩家的身份,将相应(杀手或水民)的人数 - 1 。同时判断是否满足胜利条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
btn.addEventListener("click",function () {
if(JSON.parse(localStorage.deadPlayerNum) === 0) {
alert("请选择一名玩家");
} else {
//点击按钮将死亡玩家存入数组
deadArr.push(JSON.parse(localStorage.deadPlayerNum));
localStorage.deadPlayerArr = JSON.stringify(deadArr);
//输赢判断
switch (newPlayer[parseInt(localStorage.deadPlayerNum) - 1]) {
case "杀手":
aliveKillers = parseInt(localStorage.aliveKillers);
aliveKillers--;
localStorage.aliveKillers = aliveKillers;
break;
case "水民":
aliveWaters = parseInt(localStorage.aliveWaters);
aliveWaters--;
localStorage.aliveWaters = aliveWaters;
break;
}
if(aliveWaters === 0 || aliveKillers === 0) {
//修复计时BUG
var lastTime = new Date() - new Date(localStorage.time);
localStorage.lastTime = lastTime;
btn.parentNode.setAttribute("href", "task4-6.html");
} else {
btn.parentNode.setAttribute("href", "task4-4.html")
}
}
});

大致就是这么多,没有从头开始讲所以可能不太好懂,但是一个完整的游戏也不可能把代码全部拿出来,感兴趣的可以看我的github或者与我讨论。

大概就是这么多,如有纰漏,请指正

努力<br><br>希望能成为一名前端工程师<br>加油