整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

手把手教你实现一个高性能的抽抽乐H5小游戏(含源码)

手把手教你实现一个高性能的抽抽乐H5小游戏(含源码)

我们就来学点有意思的,用几十行代码来实现一个高性能的抽奖小游戏.也基于此,来巩固我们的javascript基础,以及前端一些基本算法的应用.

效果展示



你将收获

  • 防抖函数的应用
  • 用css实现九宫格布局
  • 生成n维环形坐标的算法
  • 如何实现环形随机轨道运动函数
  • 实现加速度动画
  • 性能分析与优化

设计思路



具体实现

由于目前已有很多方案可以实现九宫格抽奖动画,比如使用动态active实现边框动画,用随机算法和定时器设置在何处停止等等. 为了进一步提高性能,本文介绍的方法,将使用坐标法,将操作dom的成本降低,完全由js实现滑块的路径的计算,滑块元素采用绝对定位,让其脱离文档流,避免其他元素的重绘等等,最后点击按钮我们会使用防抖函数来避免频繁执行函数,造成不必要的性能损失.

1. 九宫格布局实现

为了让大家更加熟悉dom结构,这里我就不用js动态生成了.如下html结构:

<div class="wrap">
    <div class="title">圣诞抽抽乐</div>
    <div class="box">
        <div class="item">我爱你</div>
        <div class="item">你爱我</div>
        <div class="item">我不爱你</div>
        <div class="item">你爱我</div>
        <div class="item start">开始</div>
        <div class="item">你爱我</div>
        <div class="item">再见</div>
        <div class="item">谢谢惠顾</div>
        <div class="item">你爱我</div>
        <div class="spin"></div>
    </div>
</div>
复制代码

九宫格布局我们使用flex来实现,核心代码如下:

.box {
    display: flex;
    flex-wrap: wrap;
    width: 300px;
    height: 300px;
    position: relative;
    .item {
        box-sizing: border-box;
        width: 100px;
    }
    // 滑块
    .spin {
        box-sizing: border-box;
        position: absolute;
        left: 0;
        top: 0;
        display: inline-block;
        width: 100px;
        height: 100px;
        background-color: rgba(0,0,0,.2);
    }
}
复制代码

由上可知容器box采用flex布局,要想让flex子元素换行,我们这里要设置flex-wrap: wrap;此时九宫格布局就实现了. 滑块采用绝对定位,至于具体如何去沿着环形轨道运动,请继续看下文介绍.

2.生成n维环形坐标的算法


由上图我们可以知道,一个九宫格的4条边,可以用以上8个坐标收尾连接起来,那么我们可以基于这个规律.来生成环形坐标集合.代码如下:


/**
 * 生成n维环形坐标
 * @param {number} n 维度
 * @param {number} cell 单位坐标长度
 */
function generateCirclePath(n, cell) {
  let arr=[]
  for(let i=0; i< n; i++) {
      arr.push([i*cell, 0])
  }
  for(let i=0; i< n-1; i++) {
      arr.push([(n-1)*cell, (i+1)*cell])
  }
  for(let i=0; i< n-1; i++) {
      arr.push([(n-i-2)*cell, (n-1)*cell])
  }
  for(let i=0; i< n-2; i++) {
      arr.push([0, (n-i-2)*cell])
  }
  return arr
}
复制代码

如果是单位坐标,那么cell为1,cell设计的目的就位为了和现实的元素相结合,我们可以手动设置单元格的宽度来实现不同大小的n维环形坐标集.

3.实现环形随机轨道运动函数

由抽奖动画分析可知,我们滑块运动的轨迹,其实就是环形坐标集合,所以我们只要让滑块的顶点(默认左上角)沿着环形坐标集合一步步变化就好了.

function run(el, path, n=1, i=0, len=path.length) {
    setTimeout(()=> {
        if(n > 0) {
          if(len <=i) {
              i=n===1 ? len : 0
              n--
          }
          el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
          run(el, path, n, ++i, len)
        }
    }, 300)   
}
复制代码

这样就能实现我们的滑块按照九宫格边框运动的动画了,当然以上函数只是基本的动画, 还没有实现在随机位置停止, 以及滑块的加速度运动,这块需要一定的技巧和js基础知识比如闭包.

3.1 加速度运动

加速度运动其实很简单,比如每转过一圈将setTimeout的延迟时间改变即可.代码如下:

function run(el, path, n=1, speed=60, i=0, len=path.length) {
    setTimeout(()=> {
        if(n > 0) {
          if(len <=i) {
              i=n===1 ? len : 0
              n--
              speed +=(300 - speed) / n
          }
          el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
          run(el, path, n, speed, ++i, len)
        }
    }, speed)   
}
复制代码

3.2 随机停止实现

随机停止这块主要是用了Math.random这个API, 我们在最后一圈的时候, 根据随机返回的数值来决定何时停止,这里我们在函数内部实现随机数值,完整代码如下:

/**
* 环形随机轨道运动函数
* @param {element} el 运动的dom元素
* @param {array} path 运动的环形坐标集合
* @param {number} speed 运动的初始速度
* @param {number} i 运动的初始位置
* @param {number} len 路径的长度
* @param {number} random 中奖坐标
*/
function run(el, path, n=1, speed=60, i=0, len=path.length, random=Math.floor(Math.random() * len)) {
    setTimeout(()=> {
        if(n > 0) {
          // 如果n为1,则设置中奖数值
          if(n===1) {
            len=random
          }
          if(len <=i) {
              i=n===1 ? len : 0
              n--
              speed +=(300 - speed) / n
          }
          el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
          run(el, path, n, speed, ++i, len, random)
        }
    }, speed)   
}
复制代码

4.实现点击开始的防抖函数以及应用

防抖函数实现:

// 防抖函数,避免频繁点击执行多次函数
function debounce(fn, interval=300) {
  let timeout=null
  return function () {
      clearTimeout(timeout)
      timeout=setTimeout(()=> {
          fn.apply(this, arguments)
      }, interval)
  }
}
复制代码

那么我们点击时,代码应该长这样:

// 点击开始按钮,开始抽奖
$('.start').on('click',debounce(()=> { run($('.spin'), generateCirclePath(3, 100), 3) }))
复制代码

延伸

在文章发布之后,有热心的小伙伴们提出了几个建议,综合如下:

  • 抽奖动画结束后提供回调来通知页面以便处理其他逻辑
  • 处理多次点击时,虽然加了防抖,但是用户在动画没结束时点击了开始按钮,又会执行动画导致动画越来越快,发生混乱.

综合以上问题,我在之前基础上做了进一步扩展,来解决以上提到的问题.

  1. 添加动画结束时回调:
/**
* 环形随机轨道运动函数
* @param {element} el 运动的dom元素
* @param {array} path 运动的环形坐标集合
* @param {func} cb 动画结束时回调
* @param {number} speed 运动的初始速度
* @param {number} i 运动的初始位置
* @param {number} len 路径的长度
* @param {number} random 中奖坐标
*/
function run(el, path, n=1, cb, speed=60, i=0, len=path.length, random=Math.floor(Math.random() * len)) {
    setTimeout(()=> {
        if(n > 0) {
          // 如果n为1,则设置中奖数值
          if(n===1) {
            len=random
          }
          if(len <=i) {
              i=n===1 ? len : 0
              n--
              speed +=(300 - speed) / n
          }
          el.css('transform', `translate(${path[i][0]}px, ${path[i][1]}px)`)
          run(el, path, n, cb, speed, ++i, len, random)
        }else {
          cb && cb()
        }
    }, speed)
}
复制代码
  1. 处理多次点击时,虽然加了防抖,但是用户在动画没结束时点击了开始按钮,又会执行动画导致动画越来越快,发生混乱.
// 1. 点击开始按钮,开始抽奖
$('.start').on('click',debounce(()=> {
    // 点击开始后禁用点击
    $('.start').css('pointer-events', 'none')
    run($('.spin'), generateCirclePath(3, 100), 3, ()=> {
      // 动画结束后开启按钮点击
      $('.start').css('pointer-events', 'auto')
      alert('抽奖结束')
    }) 
}))
复制代码

谢谢各位认真的建议,继续优化吧.

总结

该实现方式的好处是支持n维环形坐标的抽奖,基于坐标法的应用还有很多,尤其是游戏和图形领域,在实现过程中一定要考虑性能和可扩展性,这样我们就可以在不同场景使用同一套方法论,岂不乐哉?本文完整源码我会放在github上,欢迎交流学习~

github地址:https://github.com/MrXujiang?tab=repositories

欢迎在公众号《趣谈前端》加入我们一起学习讨论,共同探索前端的边界。

天给大家分享了一个用javascript和HTML5做出来的音乐播放器,今天小编我要给大家做一个童年小霸王游戏机里面的经典游戏,坦克大战,源码全部都有,希望大家自己也能够多练习,将知识变为自己的。

这里还是要说一下我的前端学习群:594959296,从我一个人到现在的1422人都是我每篇文章每个特效聚集的小伙伴,可以说都是我们大前端的学霸啊,不定期分享干货。想学到东西的都可以来,欢迎初学和进阶中的小伙伴

完整代码太长,要自己练习的加这个群:594959296 所有源码都上传到群文件了,自助下载学习,之前也上传了很多类似源码,希望大家能早日成大神

学习javascript也是有门槛的,就是你的html和css至少还比较熟练,您不能连html这东东是干啥的都不知道就开始学javascript了,学乘除前,学好加减法总是有益无害的。

再说二点建议:

  1. 不要急着看一些复杂的javascript网页特效的代码,这样除了打击你的自信心,什么也学不到

  2. 看网上什么几天精通javascript的,直接跳过吧,没戏

如果想看到更加系统的文章和学习方法经验可以关注我的微信公众号:‘web前端课程’关注后回复‘给我资料’可以领取一套完整的学习视频


览器现在为 JavaScript 开发人员提供了许多用于创建有趣站点的选项。 Flash曾经被用来做这个 - 它很流行,无数的游戏、播放器、花哨的界面等等都是在它上面创造出来的。但是,它们不再在任何现代浏览器中运行。

Flash技术重量级,漏洞百出,因此开始放弃。特别是因为有 HTML5 形式的替代方案。

Canvas 是可以使用 JS 命令在其上进行绘制的画布。它可用于创建动画背景、各种构造函数,最重要的是,游戏。

在本文中,您将学习如何使用 JavaScript 和 HTML5 创建浏览器游戏。但首先,我们建议您熟悉 JS 中的面向对象编程(只需了解什么是类、方法和对象)。这是创建游戏的最佳方式,因为它允许您使用实体而不是抽象数据。但是,有一个缺点:任何版本的 Internet Explorer 都不支持 OOP。


游戏页面布局

首先,您需要创建一个显示画布的页面。这需要很少的 HTML:

<!DOCTYPE html>
<html>
    <head>
        <title>JS Game</title>
        <link rel="stylesheet" href="style.css">
        <meta charset="utf-8">
    </head>
    <body>
        <div class="wrapper">
            <canvas width="0" height="0" class="canvas" id="canvas">Your browser does not support JavaScript и HTML5 </canvas>
        </div>
        <script src="game.js"></script>
    </body>
</html>

现在我们需要添加样式:

body, html
{
    width: 100%;
    height: 100%;
    padding: 0px;
    margin: 0px;
    overflow: hidden;
}
 
.wrapper
{
    width: 100%;
    height: 100%;
}
 
.canvas
{
    width: 100%;
    height: 100%;
    background: #000;
}

请注意,在 HTML 中,canvas 元素的宽度和高度为零,而 CSS 指定为 100%。 在这方面,画布的行为就像一个图像。 它具有实际分辨率和可见分辨率。

使用样式更改可见分辨率。 但是,图片的尺寸将保持不变:它只会被拉伸或压缩。 这就是为什么稍后将通过脚本指定实际宽度和高度的原因。


游戏脚本

首先,让我们为游戏添加一个脚本蓝图:

var canvas=document.getElementById("canvas"); //Retrieving a canvas from the DOM
var ctx=canvas.getContext("2d"); //Obtaining a context - through it you can work with the canvas
 
var scale=0.1; //Machine scale
 
Resize(); //When the page loads, the canvas size is set
 
window.addEventListener("resize", Resize); //Changing the size of the window will change the size of the canvas
 
window.addEventListener("keydown", function (e) { KeyDown(e); }); //Receiving keystrokes from the keyboard
 
var objects=[]; //An array of game objects
var roads=[]; //An array with backgrounds
 
var player=null; //The object controlled by the player - here will be the number of the object in the objects array
 
function Start()
{
    timer=setInterval(Update, 1000 / 60); //The game state will update 60 times per second - at this rate, the update of what is happening will seem very smooth
}
 
function Stop()
{
    clearInterval(timer); //Stopping the update
}
 
function Update() //Game update
{
    Draw();
}
 
function Draw() //Working with graphics
{
    ctx.clearRect(0, 0, canvas.width, canvas.height); //Clearing the canvas from the previous frame
}
 
function KeyDown(e)
{
    switch(e.keyCode)
    {
        case 37: //Left
            break;
 
        case 39: //Right
            break;
 
        case 38: //Up
            break;
 
        case 40: //Down
            break;
 
        case 27: //Esc
            break;
    }
}
 
function Resize()
{
    canvas.width=window.innerWidth;
    canvas.height=window.innerHeight;
}

该脚本包含创建游戏所需的一切:数据(数组)、更新、绘制和控制功能。 它只剩下用基本逻辑来补充它。 也就是说,准确指定对象的行为方式以及它们在画布上的显示方式。


游戏逻辑

在 Update() 函数调用期间,游戏对象的状态将发生变化。 之后,它们将使用 Draw() 函数在画布上绘制。 所以我们实际上并没有在画布上移动对象,我们绘制它们一次,然后更改它们的坐标,擦除旧图像并使用新坐标显示对象。 这一切发生得如此之快,以至于给人一种运动的错觉。

让我们看一个道路的例子。

此图像显示在画布上并逐渐向下移动。 紧接着,又会显示出另一幅这样的画面,让人感觉像是一条没有尽头的路。

为此,让我们创建一个 Road 类:

class Road
{
    constructor(image, y)
    {
        this.x=0;
        this.y=y;
 
        this.image=new Image();
        
        this.image.src=image;
    }
 
    Update(road) 
    {
        this.y +=speed; //The image shifts down when you refresh
 
        if(this.y > window.innerHeight) //If the image has gone over the edge of the canvas, change the position
        {
            this.y=road.y - this.image.height + speed; //The new position is indicated with the second background
        }
    }
}

将 Road 类的两个对象添加到背景数组中:

var roads=[
    new Road("images/road.jpg", 0),
    new Road("images/road.jpg", 626)
]; //background array

您现在可以更改 Update() 函数,以便图像的位置随每一帧而变化。

function Update() //Game Update
{
    roads[0].Update(roads[1]);
    roads[1].Update(roads[0]);
 
    Draw();
}

只需添加这些图像的输出:

function Draw() //Working with graphics
{
    ctx.clearRect(0, 0, canvas.width, canvas.height); //Clearing the canvas from the previous frame
 
    for(var i=0; i < roads.length; i++)
    {
        ctx.drawImage
        (
            roads[i].image, //Render image
            0, //Initial X position in the image
            0, //Initial Y-axis position in the image
            roads[i].image.width, //Image width
            roads[i].image.height, //Image height
            roads[i].x, //X-axis position on the canvas
            roads[i].y, //Y-axis position on the canvas
            canvas.width, //The width of the image on the canvas
            canvas.width //Since the width and height of the background are the same, the width is specified as the height
        );
    }
}

现在你可以看到它在游戏中是如何工作的:

现在是添加玩家和 NPC 的时候了。 为此,您需要编写一个 Car 类。 它将有一个 Move() 方法,玩家可以使用该方法控制他的汽车。 NPC 的移动将通过 Update() 完成,它只是更改 Y 坐标。

class Car
{
    constructor(image, x, y)
    {
        this.x=x;
        this.y=y;
 
        this.image=new Image();
 
        this.image.src=image;
    }
 
    Update()
    {
        this.y +=speed;
    }
 
    Move(v, d) 
    {
        if(v=="x") //X-axis movement
        {
            this.x +=d; //Offset
 
            //
            if(this.x + this.image.width * scale > canvas.width)
            {
                this.x -=d; 
            }
    
            if(this.x < 0)
            {
                this.x=0;
            }
        }
        else //Y-axis movement
        {
            this.y +=d;
 
            if(this.y + this.image.height * scale > canvas.height)
            {
                this.y -=d;
            }
 
            if(this.y < 0)
            {
                this.y=0;
            }
        }
        
    }
}

让我们创建第一个要检查的对象。

var objects=[
    new Car("images/car.png", 15, 10)
]; //An array of game objects
var player=0; //the number of the object controlled by the player

现在您需要向 Draw() 函数添加一个用于绘制汽车的命令。

for(var i=0; i < objects.length; i++)
{
    ctx.drawImage
    (
        objects[i].image, //Render image
        0, //Initial X position in the image
        0, //Initial Y-axis position in the image
        objects[i].image.width, //Image width
        objects[i].image.height, //Image height
        objects[i].x, //X-axis position on the canvas
        objects[i].y, //Y-axis position on the canvas
        objects[i].image.width * scale, //The width of the image on the canvas multiplied by the scale
        objects[i].image.height * scale //The height of the image on the canvas multiplied by the scale
    );
}

在按下键盘时调用的 KeyDown() 函数中,您需要添加对 Move() 方法的调用。

function KeyDown(e)
{
    switch(e.keyCode)
    {
        case 37: //Left
            objects[player].Move("x", -speed);
            break;

        case 39: //Right
            objects[player].Move("x", speed);
            break;
 
        case 38: //Up
            objects[player].Move("y", -speed);
            break;
 
        case 40: //Down
            objects[player].Move("y", speed);
            break;
 
        case 27: //Esc
            if(timer==null)
            {
                Start();
            }
            else
            {
                Stop();
            }
            break;
    }
}

现在您可以检查渲染和控制。

碰撞时什么都没有发生,但这将在以后修复。 首先,您需要确保删除视图中丢失的对象。 这是为了避免堵塞 RAM。

在 Car 类中,我们添加值为 false 的字段 dead,然后在 Update() 方法中对其进行更改:

if(this.y > canvas.height + 50)
{
    this.dead=true;
}

现在您需要更改游戏的更新功能,替换与对象关联的代码:

var hasDead=false;
 
for(var i=0; i < objects.length; i++)
{
    if(i !=player)
    {
        objects[i].Update();
 
        if(objects[i].dead)
        {
            hasDead=true;
        }
    }
}
 
if(hasDead)
{
    objects.shift();
}

如果您不移除物体,当生成太多汽车时,游戏将开始降低计算机速度。


游戏物体碰撞

现在您可以开始实施碰撞。 为此,请为 Car 类编写一个方法 Collide(),它将检查汽车的坐标:

Collide(car)
{
    var hit=false;
 
    if(this.y < car.y + car.image.height * scale && this.y + this.image.height * scale > car.y) //If the objects are on the same line horizontally
    {
        if(this.x + this.image.width * scale > car.x && this.x < car.x + car.image.width * scale) //If the objects are on the same line vertically
        {
            hit=true;
        }
    }
 
    return hit;
}

现在我们需要在 Update() 函数中添加碰撞检查:

var hit=false;
 
for(var i=0; i < objects.length; i++)
{
    if(i !=player)
    {
        hit=objects[player].Collide(objects[i]);
 
        if(hit)
        {
            alert("You crashed!");
            Stop();
            break;
        }
    }
}

这是游戏中的内容

碰撞时可以添加任何逻辑:

? 打开动画;

? 添加效果;;

? 删除对象;

? 健康状况的改变,等等。

所有这些都由开发人员自行决定。


结论

这是一个非常简单的游戏,但足以了解 JS 如何处理图形以及一般如何创建游戏。 您可以在 GitHub 存储库中找到图像和完整的游戏代码。

使用画布非常适合处理图形:它提供了很棒的功能并且不会过多地加载浏览器。 我们现在也有一个可用的 WebGL 库(示例和用法),它可以为您提供大量的性能和 3D 工作(canvas 无法做到这一点)。

理解 WebGL 可能很困难——也许相反,许多人对尝试 Unity 引擎更感兴趣,它知道如何编译项目以在浏览器中运行它们。


关注七爪网,获取更多APP/小程序/网站源码资源!