w3ctech

我是怎样实现Coreball游戏的

我是怎样实现Coreball游戏的

首先我声明一下,我不是原作者,实现代码仅供参考和研究,原版是用canvas画的,我这版是用DOM实现,而且只有1关作为示例。

玩原版coreball游戏请到这里—— http://coreball.sinaapp.com/


HTML结构: 旋转的列表

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Coreball</title>
</head>
<body>
  <div>
      <ul id="levelballs">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
      </ul>
      <ul id="addedballs">
        <li>8</li>
        <li>7</li>
        <li>6</li>
        <li>5</li>
        <li>4</li>
        <li>3</li>
        <li>2</li>
        <li>1</li>
      </ul>
  </div>
</body>
</html>

结构中采用两个ul来分别表示旋转的“风车”,以及要添加的一溜小球,每个li表示一个小球。

不过现在显示出来的样子是这样的——

img

缺少css,裸奔果然丑爆了有木有?

啥也别说了,赶紧加css呗——

html, body{
  height: 100%;
  overflow: hidden;
  background: black;
  -webkit-user-select: none;
}

/*大球的通用样式*/
ul {
  background: white;
  color: black;
  list-style-type:none;
  padding: 0;
}

/*小球的通用样式*/
ul >li {
  width: 40px;
  height: 40px;
  border-radius:50%;
  background: white;
  line-height: 40px;
  font-size: 1.5rem;
  text-align: center;
}

/*大球的样式*/
#levelballs {
  width: 80px;
  height: 80px;
  margin: 220px auto;
  border-radius:50%;
  -webkit-transform: rotate(45deg);
}

/*大球周围的小球*/
#levelballs >li {
  position: absolute;
  float: left;
  margin: 20px 0 0 240px;
  -webkit-transform-origin: -200px 20px;
}

#levelballs >li:nth-child(1){
  -webkit-transform: rotate(0deg);
}

#levelballs >li:nth-child(2){
  -webkit-transform: rotate(90deg);
}

#levelballs >li:nth-child(3){
  -webkit-transform: rotate(180deg);
}

#levelballs >li:nth-child(4){
  -webkit-transform: rotate(270deg);
}

/*玩家添加的小球*/
#addedballs {
  width: 40px;
  margin: 0 auto;
  background: transparent;
}

#addedballs >li {
  margin-bottom: 10px;
}

加了上面一坨CSS后,界面长这样——

img

好看一点了有木有?稍微解说一下关键的CSS:

白色圆球:

border-radius:50%;
background: white;

小球以大球圆心围绕大球旋转:

position: absolute;
float: left;
margin: 20px 0 0 240px;
-webkit-transform-origin: -200px 20px;

四颗小球的旋转位置

#levelballs >li:nth-child(1){
  -webkit-transform: rotate(0deg);
}

#levelballs >li:nth-child(2){
  -webkit-transform: rotate(90deg);
}

#levelballs >li:nth-child(3){
  -webkit-transform: rotate(180deg);
}

#levelballs >li:nth-child(4){
  -webkit-transform: rotate(270deg);
}

这样,我们就得到了小球和大球,但是小球和大球之间需要有一根连线,这根连线可以用:before伪元素来实现:

/*小球与大球中间的连线*/
#levelballs >li:before {
  content: "";
  float: left;
  display: block;
  width: 160px;
  height: 1px;
  margin: 20px 0 0 -160px;
  background: white;
}

于是界面长这样了——

img

小球的运动:一二三,转!

之前月影有道面试题,大概是如何让一个小球以某个固定点为圆心,R为半径旋转,好像很少有人回答出满意的答案,但其实这个问题有很多答案,最简单的方式是用css3动画:

@-webkit-keyframes rotate{
  from {-webkit-transform:rotate(0deg);}
  to {-webkit-transform:rotate(360deg);}
}

#levelballs.play {
  -webkit-animation-name: rotate;
  -webkit-animation-duration: 5.0s;
  -webkit-animation-iteration-count: infinite;
  -webkit-animation-timing-function: linear;  
}

这样的话,小球就可以旋转起来了。

JavaScript实现的交互

因为levelballs通过css动画实现旋转,需要获取它的旋转角度,小球从正下方添加到levelballs中时,它的转角与levelballs的转角的关系为小球的转角等于90度减去levelballs的转角——

function appendBall(){
  if(balls.length){
    var deg = 90 - getRotationOf(levelballs);
    balls[0].style.webkitTransform = 'rotate(' + deg + 'deg)';
    levelballs.appendChild(balls[0]);
  }
}

img

接下来是实现 getRotationOf(levelballs),这是一个数学问题:

function getRotationOf(el){
  var style = window.getComputedStyle(el);
  if(style){
    var prop = style.getPropertyValue("-webkit-transform") || style.getPropertyValue('transform');
    prop = /matrix\((.*)\)/g.exec(prop)[1].split(',');
    prop = Math.round(Math.atan2(prop[1], prop[0]) * (180/Math.PI));
    return prop;
  }
}

基本原理并不复杂,拿到小球旋转的transform变换矩阵,再通过矩阵算出此时levelballs旋转的角度,具体公式可以看这篇文章

img

接下来是完整的代码:

void function(){'use strict'

  function getRotationOf(el){
    var style = window.getComputedStyle(el);
    if(style){
      var prop = style.getPropertyValue("-webkit-transform") || style.getPropertyValue('transform');
      prop = /matrix\((.*)\)/g.exec(prop)[1].split(',');
      prop = Math.round(Math.atan2(prop[1], prop[0]) * (180/Math.PI));
      return prop;
    }
  }

  function appendBall(){
      var balls = document.getElementById("addedballs").getElementsByTagName('li');
    if(balls.length){
      var deg = 90 - getRotationOf(levelballs);
      if(deg > 180) deg -= 360;  //角度从 -180 ~ 180
      balls[0].style.webkitTransform = 'rotate(' + deg + 'deg)';
      levelballs.appendChild(balls[0]);
    }
  }

  document.documentElement.addEventListener('touchstart', appendBall);
  document.documentElement.addEventListener('mousedown', appendBall);
}();

现在我们已经可以把小球给添加到大球上了,实际的操作非常简单,就是将小球(li)从addedballs列表中取出来,然后append到levelballs列表上去。

碰撞检测

接下来我们要做碰撞检测了,小球添加上去的时候不能碰到其他的小球,这也是一个比较简单的数学问题——

//碰撞检测              
function testBalls(deg){
  var balls = document.getElementById("levelballs").getElementsByTagName('li');

  for(var i = 0; i < balls.length; i++){
    var d = getRotationOf(balls[i]);
    if(Math.abs(deg - d) <= 10 || 
      Math.abs(deg + 360 - d) <= 10){
      return false;
    }
  }
  return true;
}

因为小球的半径r是20px,小球圆心与大球圆心的距离R是160+20+40=220px,避免碰撞的近似公式满足 2r >= d * R,所以 d >= 2r / R 大约是 0.182 单位是弧度,转换成角度是 0.182 * 180 / 3.14 = 10.4,所以碰撞的标准就是角度相差小于等于10度,因为角度是周期的,所以做如下判断——

if(Math.abs(deg - d) <= 10 || 
  Math.abs(deg + 360 - d) <= 10){
  return false;
}

img

结束游戏

有了碰撞检测,接下来我们就可以添加停止游戏的逻辑了——

function gameOver(state){
  setTimeout(function(){
    //这里延迟停止动画,不然的话appendChild异步插入,会出问题
    levelballs.className += ' gameover';
  });

  document.body.className = state;
  document.documentElement.removeEventListener('touchstart', appendBall);
  document.documentElement.removeEventListener('mousedown', appendBall);
  document.body.addEventListener('touchstart', location.reload.bind(location));
  document.body.addEventListener('mousedown', location.reload.bind(location)); 
}

游戏结束时,注销事件,并通过className改变css控制展现效果。改变appendBall方法,增加——

function appendBall(){
  var balls = document.getElementById("addedballs").getElementsByTagName('li');
  if(balls.length){
    var deg = 90 - getRotationOf(levelballs);
    if(deg > 180) deg -= 360;  //角度从 -180 ~ 180
    var pass = testBalls(deg);
    balls[0].style.webkitTransform = 'rotate(' + deg + 'deg)';
    levelballs.appendChild(balls[0]);
    if(pass){
      //成功插入
      if(balls.length <= 0){ //所有的小球都插入了
        gameOver("win");
      }
    }else{
      //失败
      gameOver("lose");
    }
  }
}

修改css,添加——

#levelballs.gameover {
  -webkit-animation-play-state:paused; 
}

body.win, body.lose {
  -webkit-user-select: none;
  -webkit-transition-property: background-color;
  -webkit-transition-duration: 0.7s;
  -webkit-transition-timing-function: ease-in-out;
}

body.win {
  background: green;
}

body.lose {
  background: red;
}

最后一个小问题,因为在PC上开发的,发现在手机上显示太大了,改一下meta头——

<meta name="viewport" content="width=device-width, initial-scale=0.6, maximum-scale=0.6, user-scalable=0" />

这样,整个简单的coreball游戏主体功能就完成啦~

完整代码和在线演示在这里

欢迎讨论,谢谢大家~

w3ctech微信

扫码关注w3ctech微信公众号

共收到3条回复

  • 666 ( ̄︶ ̄)↗ 涨

    回复此楼
  • 又学到了新知识~(≧▽≦)/~

    回复此楼
  • cool

    回复此楼