扎针小游戏 基本版

终于写出来了扎针小游戏,看起来没有太多行的代码,对于我来说真的是来之不易。在看了很多教程,也跟着写了一些小示例之后,我以为基本学习了 函数 、循环、数组 ....... 的我可以去动手写了。但其实我只能达到看一段代码能分辨这是个函数还是数组,或者好一点知道这个函数大概做了什么事情,但并不清楚函数之间怎样来回调取和运作的?我自己该从哪开始写?该怎么写?

当我被迫要去写的时候,我根本无从下手,甚至焦虑导致连基本的写法也忘记。男票说你开始写吧,我俩的对话+(心理状态)是这样的:
NP: 你试试吧 (要让她迈出第一步,总跟着例子照抄没有用)
我:好 (哎,我肯定试不出来的,他又该对我失望了)
NP:你先写,不知道怎么写的时候问我
我:好(晕,不敢讲出来,其实我第一句写啥都不知道)

......过了一段时间后......
NP: 怎么样了,有问题吗?
我:还没有...(不敢告诉他不会,我还是自己去找找例子看看该写什么吧)

......又过了一段时间后......
我:我不知道该怎么写?
NP:写什么不知道怎么写了?(你不告诉我我怎么教你)
我: 不说话(我要是能描述写不出来什么就好了, 其实我根本不知道要从哪开始写什么?)
......

先反省采取的不推荐的方法一——东拼西凑法:

向别人学习是应该的,但是学习和抄作业完全是两周不同的做法。
我因为真的不知道怎么写,就从网上找了这个游戏的源码文件,想看别人怎么写,然后希望能先模仿着别人的代码写出自己的。这个文件看不懂了或者写的没作用了,就换另一个文件,把里面看起来有用或者能看懂的都搬过来。

这时候我犯的错误是,代码里很多东西并不是独立的而是一环套一环的,一些元素、变量在一个地方定义了,会在很多其它地方调用或反复调用;有些函数作用域的关系没有搞清楚;有些示例里的写法很高级各种定义闭包父子关系,我很难理解;有一些算法或变量我知道是做什么的,但是不知道究竟是怎么算,算好之后要怎么用。所以为了能使一段运行起来,我需要吧这个代码里设计到元素的所有定义的代码全写了,然后代码只能越抄越多,后来就变成了完全照抄,根本没有理解。

男票每过一段时间过来看我写的代码的时候,都会发现我代码里突然有莫名其妙地出现一个新的完全没定义的函数或者变量,因为我又从一个源码文件里抄了一段,希望能在我这跑起来。而且我因为觉得自己跟着写的一点都没写错但是就是跑步起来,而陷入深深的挫败感

后来他让我全部删掉,从基本的画面元素开始绘制,再考虑该加入什么。然后再考虑让元素动起来。我按照他的方法尝试去拆分任务,去一步步攻破,终于理解了这个基本的可以操作的小游戏的逻辑,在此按照我一个新手小白的逻辑和理解,分享一下思考过程还有游戏源码:

首先我们需要建一个HTML文件,并定义基本的画布样式、位置、尺寸、引入JS文件

<!DOCTYPE html>
<html Ding="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Tip</title>
     <style>
           body{
        background:#eee;
    }
     #canvas {
         background: #fff;};
        cursor: pointer;
    </style>

</head>
<body>
    <canvas id = 'canvas' width='375' height='667'>
        canvas not supported
    </canvas>
    <script src = 'tip.js'></script>
</body>
</html>

这是最前面属于基础的操作,就像开始绘画前需要裁好画布,而引入的JS文件就相当于画面内容了。

开始写游戏之前我们大概要知道我们写的游戏包含什么元素,长什么样子,例如我已经构思好了游戏大概的页面样式如图


扎针界面.png

那我们先分析一下,需要我们绘制的分别都是什么?
1、中间红色大圆
2、底部蓝色小圆
3、红色圆上扎的黑色针
4、黑色针顶端发射上的蓝色小圆

我们先建一个名字为tip.js的文件,开始从最简单的1、2两个圆绘制:

//指定画布获取ID
var canvas = document.getElementById('canvas');
             // 获取id为canvas的canvas标签并赋值,指定在哪里画
    context = canvas.getContext('2d');
            //返回一个用于在画布上绘图的环境,2d表示二维绘图,指定画出的图案类型


//#绘制红色大圆
context.beginPath();             //baginPath()方法开始一条路径,或重置当前路径。
context.arc(canvas.width/2, canvas.height/4, 40, 0, Math.PI*2, true);
                                //arc(圆中心x坐标,圆中心y坐标,半径,开始角度,结束角度,绘制顺时针)
context.fillStyle = 'red';      //路径的填充颜色
context.fill();                 //fill()方法填充该路径,默认黑色


//#绘制绿色小圆
context.beginPath();
context.arc(canvas.width/2, canvas.height/2, 20, 0, Math.PI*2, true);
context.fillStyle = 'green';
context.fill();

我们看到绘制两个圆的时候都要基于画布的尺寸定位x、y坐标、半径, 那我们可以吧这三个元素定义为变量,方便后面调取。
var centerX = canvas.width/2
var centerY = canvas.height/4
var radius = 40
这样,我们在绘制大圆和小圆的时候可以调取这三个变量,更便捷。

//指定画布获取ID
var canvas = document.getElementById('canvas');
           // 获取id为canvas的canvas标签并赋值,指定在哪里画
   context = canvas.getContext('2d');
           //返回一个用于在画布上绘图的环境,2d表示二维绘图,指定画出的图案类型

//定义变量

var centerX = canvas.width/2;
var centerY = canvas.height/4;
var radius = 40;

//绘制红色大圆
function drawBig(){
       context.beginPath();             //baginPath()方法开始一条路径,或重置当前路径。
       context.arc(centerX, centerY, radius, 0, Math.PI*2, true);
                               //arc(圆中心x坐标,圆中心y坐标,半径,开始角度,结束角度,绘制顺时针)
       context.fillStyle = 'red';      //路径的填充颜色
       context.fill();                 //fill()方法填充该路径,默认黑色
}

//绘制绿色小圆
function drawSmall(){
       context.beginPath();
       context.arc(centerX, centerY*3, radius/2, 0, Math.PI*2, true);
       context.fillStyle = 'green';
       context.fill();
}

//绘制指针和发射上的小圆

function drawLine(angle){
       var x = centerX + Math.cos(angle)*(radius+60);
       var y = centerY + Math.sin(angle)*(radius+60);
       context.beginPath();
       context.moveTo(centerX, centerY);
       context.lineTo(x, y);
       context.stroke();
       context.closePath();
       context.arc(x, y, radius/2, 0, Math.PI*2, true);
       context.fill();
}

现在还需要我们绘制的有
3、红色圆上扎的黑色针
4、黑色针顶端发射上的蓝色小圆

我们需要先进行第2步绘制出指针,然后在绘制直镇上的小圆,因为第4步黑色针顶端发射上的蓝色小圆是要基于第3步指针的位置判断的。

用canvas里绘制指针其实就是绘制线,需要用到的方法有 moveTo()、lineTo()
需要指针从大圆的中心点开始向外延伸绘制,如图所示,结束点=大圆中心点位置 +相交点位置+相交点到结束点显露的指针长度。

相交点的(x,y)需要用到三角函数计算出不同角度时和圆相交的位置。
指针长度可以直接加上一个数值,该数值就是最圆外伸出的指针的实际长度。


三角函数.png

为了方便起见,我们把相交点x、y定义为一个局部变量
var x = canvas.width/2 + Math.cos(angle)(radius+60)
其中canvas.width/2可以直接调取上层的全局变量center获取,也就是可以写成
var x = centerX+Math/2 + Math.cos(angle)
(radius+60)
var y = centerY+Math.sin(angle)*(radius+60)

//绘制指针和发射到指针上的小圆
var angle =30;
var x = centerX + Math.cos(angle)*(radius+60);    //加号内容用小括号(),会优先计算
var y = centerY + Math.sin(angle)*(radius+60);
context.beginPath();
context.moveTo(centerX, centerY);
context.lineTo(x, y);                                             //调取变量var x、var y
context.stroke();
                                     //需要重新开始一段新的路径,否则旋转时会有因为指
context.beginPath();                //针路径没结束,就开始绘制小圆,导致路径绘制错误    
context.arc(x, y, radius_small, 0, 2 * Math.PI, false);  //从结束点的x、y开始绘制小圆
context.fill();

开始定义函数

函数:function functionname(参数){...函数体...},当调用该函数时,会执行函数内的代码,调用函数式,可以向其传递值,这些值被称为参数。声明函数时,请把参数作为变量来声明。

因为现在绘制指针的角度angle是定义了一个具体值30,但是我们需要环绕着绘制很多根指针,这时候我们需要一个for循环的函数,能够循环操作好多遍指针。那我们这时候就需要把绘制指针的代码放到一个函数里,才能在for循环里调取函数执行绘制。

//绘制指针和发射上的小圆
function drawLine (angle){   //定义名为drawLine的函数,可直接调取drawLine,函数里可以传入angle的参数
        var x = centerX + Math.cos(angle)*(radius+60);
        var y = centerY + Math.sin(angle)*(radius+60);
        context.beginPath();
        context.moveTo(centerX, centerY);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();
        context.arc(x, y, radius/2, 0, Math.PI*2, true);
        context.fill();
}

此时我们可以考虑把绘制的大圆、小圆也都定义成一个函数,然后把大圆、小圆、指针、指针上的圆都包到一个大的函数里,这样可以统一调取该函数,绘制所有内容。
记得定义成函数之后要调取函数,函数才会执行,如我定义了function drawBig(){...},需要在后面调取 drawBig(); 要么是没法执行的,也看不到画布上有什么内容。

//指定画布获取ID
var canvas = document.getElementById('canvas');
            // 获取id为canvas的canvas标签并赋值,指定在哪里画
    context = canvas.getContext('2d');
            //返回一个用于在画布上绘图的环境,2d表示二维绘图,指定画出的图案类型

//定义变量

var centerX = canvas.width/2;
var centerY = canvas.height/4;
var radius = 40;

//绘制红色大圆
function drawBig(){
        context.beginPath();             //baginPath()方法开始一条路径,或重置当前路径。
        context.arc(centerX, centerY, radius, 0, Math.PI*2, true);
                                //arc(圆中心x坐标,圆中心y坐标,半径,开始角度,结束角度,绘制顺时针)
        context.fillStyle = 'red';      //路径的填充颜色
        context.fill();                 //fill()方法填充该路径,默认黑色
}

//绘制绿色小圆
function drawSmall(){
        context.beginPath();
        context.arc(centerX, centerY*3, radius/2, 0, Math.PI*2, true);
        context.fillStyle = 'green';
        context.fill();
}

//绘制指针和发射上的小圆

function drawLine(angle){
        var x = centerX + Math.cos(angle)*(radius+60);
        var y = centerY + Math.sin(angle)*(radius+60);
        context.beginPath();
        context.moveTo(centerX, centerY);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();
        context.arc(x, y, radius/2, 0, Math.PI*2, true);
        context.fill();
}

for(var i = 0; i < 18; i++){  
        drawLine(Math.PI /7 * i);    //执行的函数: 传给drawLine(angle) 的参数为:开始的角度为等分每次乘以一个变量i
        }
        
        drawBig();     //执行函数drawBig
        drawSmall();
        //drawLine();   //此函数在for循坏内已被调用,所以此处不用再调用了
        

重点解释一下for循环的部分
循环:希望一遍遍的运行相同的代码,并且每次的值都不同。
for(var i = 0; i < 18; i++) 其中()内三个值分别为:
1、在循环开始前执行(设定变量)初始var i = 0;
2、循环运行的条件 i<18 ,只要I小于18,就会继续执行下一步,知道不小于18了就停止执行循环;
3、在每次代码已被执行后增加一个值(i++)每次i都加1个去执行下一步;

{
drawLine(Math.PI /7 * i);
}
花括号内是具体执行的函数:,此处执行了函数draw,并且传给drawLine(angle) 的参数为,Math.PI/7 是初始的角度,相当于180度➗7等于30度,然后30度✖️i ,i每次都会+1,其实每次传入的angle值分别为:
1、30✖️0=0
2、30✖️(0+1)=30
3、30✖️(1+1)=60
4、30✖️(2+1)=90
5 、...
12、30✖️(10+1)=330
然后在每传入的angle值相对于上次angle值增加了30度,传入angle之后执行一次函数drawLine,在新的角度绘制一条新的线。

此时效果如截图:


image.png

那么接下来该怎么让指针动起来呢?

转动的原理其实是:每次转动我们需要把原来的内容清除,转动一个角度到新位置,再重新绘制元素到新的位置上。那我们需要做的事情可以分几步:
1、我们把需要清除并重新绘制的画布元素统一封装在一个总函数内function draw(startAngle) 内,注意这里的封装不是行程父子关系,而是为了便于管理并且能同意传入参数,总函数内的函数都可调用获取该参数。
2、给函数draw传入一个角度旋转的参数draw(rotateAngle),让其每次绘制都能比上一步旋转一个角度进行绘制不断。角度初始为0,每次执行一次在角度基础上-0.01传入。
3、每次函数执行一次后要clearRect()清除画布再重新绘制,需要卸载总函数的第一部,这样每次执行新的绘制前就先清除画布原有内容。
4、在动画内使用requestAnimationFrame()方法,并且在函数内给draw传入角度参数。
functionframe(){
requestAnimationFrame(frame);
}

//指定画布获取ID
var canvas = document.getElementById('canvas');
            // 获取id为canvas的canvas标签并赋值,指定在哪里画
    context = canvas.getContext('2d');
            //返回一个用于在画布上绘图的环境,2d表示二维绘图,指定画出的图案类型

//封装成总函数  + 定义变量
function draw(rotateAngle){
        context.clearRect(0, 0, canvas.width, canvas.height);
        var centerX = canvas.width/2;
        var centerY = canvas.height/4;
        var radius = 40;

        //绘制红色大圆
        function drawBig(){
        context.beginPath();             //baginPath()方法开始一条路径,或重置当前路径。
        context.arc(centerX, centerY, radius, 0, Math.PI*2, true);
                                //arc(圆中心x坐标,圆中心y坐标,半径,开始角度,结束角度,绘制顺时针)
        context.fillStyle = 'red';      //路径的填充颜色
        context.fill();                 //fill()方法填充该路径,默认黑色
        }

        //绘制绿色小圆
        function drawSmall(){
        context.beginPath();
        context.arc(centerX, centerY*3, radius/2, 0, Math.PI*2, true);
        context.fillStyle = 'green';
        context.fill();
        }

//绘制指针和发射上的小圆
        function drawLine(angle){
        var x = centerX + Math.cos(angle)*(radius+60);
        var y = centerY + Math.sin(angle)*(radius+60);
        context.beginPath();
        context.moveTo(centerX, centerY);
        context.lineTo(x, y);
        context.stroke();
        context.closePath();
        context.arc(x, y, radius/2, 0, Math.PI*2, true);
        context.fill();
        }

        for(var i = 0; i < 18; i++){
        drawLine(Math.PI/6 * i + rotateAngle);  //这里原本是按照30度*变量i的角度去绘制了多根指针线,但想要指针转动起来,
                                                //就需要告诉drawLine按什么角度旋转才能动起来,因此需+rotateAngle的参数
        }

        drawBig();
        drawSmall();
        }

var rotateAngle = 0;
function frame(){
        rotateAngle -= 0.01;   // -=    rotateAngle每次减去0.01再传给rotateAngle,获得新的旋转角度
        draw(rotateAngle);     //传给函数draw的旋转参数
        requestAnimationFrame(frame);  //执行动画循环播放的方法,需要回调函数frame
}
frame();
image.png

此时的效果如截图,并且指针是可以跟着旋转的了,

TIP: 此时会有一个其实不关键的区别,会引起一些疑惑,原本扎在指针上的小圆是黑色的,那为什么现在旋转起来之后小圆变成绿色的了?
我们看一下drawLine的函数里,执行了fill(),但是没有设定fillStyle的颜色,如果没设定的话,系统会默认用黑色填充,这就是为什么最初显示的是黑色的小球。那我们现在加入了旋转的函数,会一遍遍的去执行绘制的函数,这时候也就是从第二遍开始,会用我们系统里最后一次定义的fillStyle的颜色去填充小圆的fill()。返回代码可以看到,最后一次是在函数drawSmall的函数里定义了小圆的fillStyle= 'green',所以第二帧开始就会是绿色的。其实此时第一帧还是初始状态即没有旋转的状态还是黑色的,第二帧开始才是绿色的,只是视觉上识别不到这一帧的区别。如果觉得不严谨,那可以在drawLine函数里自行定义fillStyle= 'green',那么第一帧到最后的每一帧都是这个green的颜色。

增加鼠标监听事件

我们需要达到的效果是每点击一次鼠标发射一次小圆扎在大圆上,也就是每点击一次鼠标绘制一个指针加球,这时候需要做的事情是:
1、增加鼠标监听,点击一次在新的角度绘制一个指针
2、需要计算新指针发射(绘制)的角度。
3、需要一个数组存放绘制的指针的角度。
4、需要用for循环遍历数组并重复执行绘制。
5、用 if 判断,当新绘制的指针与任一个指针之间间距小于某个数值的时候,游戏结束,否则就继续游戏。注意因为圆盘是旋转的,所以发射指针时可能会与人一个已有指针相撞,因此需要获取数组内所有的指针角度。

解释:
1、监听鼠标事件需要用到 addEventListener('click',function(){...})
方法,用于向指定元素添加事件。
—— ‘click’的事件必须,一般是一个指定事件名的字符串。注意: 不要使用 "on" 前缀。 例如,使用 "click" ,而不是使用 "onclick"。
—— function必须,指定要事件触发时执行的函数。 当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。例如, "click" 事件属于 MouseEvent(鼠标事件) 对象。

2、假定我需要指针从底部往上发,那就是要在Math.PI/2 =90度的位置绘制,因为圆盘在旋转,所以还需要90度减去rotateAngle,才能每次都发射到着下方的垂直位置

3、先定义一个默认包含角度0和180两个角度元素的数组needles
needles = [{
angle: 0
}, {
angle: Math.PI
}],
之后所有发射(绘制)的指针的角度都会存放在数组needles里,定义让tip对象等于数组内的参数。 var tip = needles[ i ]; 然后就可以通过调取tip.angle来获取数组里所有角度参数。

然后每次绘制一根新的指针,实际都需要往数组内最后一位传入一个角度,获取到新的角度并在该角度绘制新的指针。

4、此处的for循环主要是为了遍历数组,遍历数组之后就可以获得数组内的角度值。基本的写法是:
for(var i = 0; i <needles.length; i++){...}
但是这样写不太好的原因是,这样每次都要取数组的长度再去执行,我们试着换巧妙一点的方式可以改写为:
for(var i = needles.length; i>=0; i--){...}
这种写法倒序来遍历,并且不用每次都获取数组长度,也节省了一个暂时变量。

5、这里的逻辑文字解释是:当我新发射的指针角度和转盘上已有的指针角度的差小于0.3(可根据大小和角度自己给定合适的数字)的时候,游戏就结束,isGameOver = true,弹出'Game Over',如果没有结束 !isGAMEOver,那就可以继续往数组里传入一个新的角度,继续绘制。

//指定画布获取ID
var canvas = document.getElementById('canvas');
           // 获取id为canvas的canvas标签并赋值,指定在哪里画
   context = canvas.getContext('2d');
           //返回一个用于在画布上绘图的环境,2d表示二维绘图,指定画出的图案类型
   needles = [{     
           angle: 0    
   },{
           angle: Math.PI      //默认初始在0和180度有两根指针,如果想默认只有一根,删掉一个数组元素即可
   }],
   isGameOver = false;         //游戏结束的时候就不执行函数了

//封装成总函数  + 定义变量
function draw(rotateAngle){
       context.clearRect(0, 0, canvas.width, canvas.height);
       var centerX = canvas.width/2;
       var centerY = canvas.height/4;
       var radius = 40;

       //绘制红色大圆
       function drawBig(){
       context.beginPath();             //baginPath()方法开始一条路径,或重置当前路径。
       context.arc(centerX, centerY, radius, 0, Math.PI*2, true);
                               //arc(圆中心x坐标,圆中心y坐标,半径,开始角度,结束角度,绘制顺时针)
       context.fillStyle = 'red';      //路径的填充颜色
       context.fill();                 //fill()方法填充该路径,默认黑色
       }

       //绘制绿色小圆
       function drawSmall(){
       context.beginPath();
       context.arc(centerX, centerY*3, radius/2, 0, Math.PI*2, true);
       context.fillStyle = 'green';
       context.fill();
       }

       //绘制指针和发射上的小圆
       function drawLine(angle){
       var x = centerX + Math.cos(angle)*(radius+60);
       var y = centerY + Math.sin(angle)*(radius+60);
       context.beginPath();
       context.moveTo(centerX, centerY);
       context.lineTo(x, y);
       context.stroke();
       context.closePath();
       context.arc(x, y, radius/2, 0, Math.PI*2, true);
       context.fill();
       }

       /*
       for(var i=0; i<18; i++){
               drawLine(Math.PI/6*i + rotateAngle);//这里原本是按照30度*变量i的角度去绘制了多根指针线,但想要指针转动起来,
       }                                       //就需要告诉drawLine按什么角度旋转才能动起来,因此需+rotateAngle的参数
       */

       for(var i = 0; i < needles.length; i++){     //因为此时扎针的角度是随机且不均分的,所以需要使用数组长度,当数组内有3个元素时,i<3就执行绘制第三根,
                                                    //所以数组的长度就是判断是否继续执行的条件
               var tip = needles[i];                //可以通过 tip获取数组的角度参数,在下一步可直接调取 tip.angle
               drawLine(tip.angle + rotateAngle);  // tip.angle是数组内需要绘制指针的角度
                                              
       }

       drawBig();
       drawSmall();
       drawLine();
}

       var rotateAngle = 0;
       function frame(){
       rotateAngle -= 0.01;   // -=    rotateAngle每次减去0.01再传给rotateAngle,获得新的旋转角度
       draw(rotateAngle);     //传给函数draw的旋转参数
       requestAnimationFrame(frame);  //执行动画循环播放的方法,需要回调函数frame
       }
       frame();

canvas.addEventListener('click',function(){
var angle = Math.PI/2 - rotateAngle ;
for( var i = needles.length-1; i>= 0; i--){
       var tip = needles[i];
       if(Math.abs(angle - tip.angle) < 0.3){
               isGameOver = true;
               alert('Game Over');
       }
       }

       if (!isGameOver){
               numbers.push({
                       angle:angle
               });
       }

});

image.png

推荐阅读更多精彩内容