8

用原生 JS 写一个简易版的台球

 1 year ago
source link: https://www.51cto.com/article/720956.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

用原生 JS 写一个简易版的台球

作者:逍丶 2022-10-20 11:49:49
requestAnimationFrame就是一个JS动画帧,简单来说和定时器有点相似,但是动画呈现出来的效果比定时器更流畅,性能更好。
356bc35974bd82026b82865541a5d7dcc5818c.png

突发奇想想用JS写一个台球小游戏,磕磕碰碰之后,算是实现了一个简易版的。用到的知识主要是通过递归来调用requestAnimationFrame,以及一些简单的三角函数角度计算。requestAnimationFrame就是一个JS动画帧,简单来说和定时器有点相似,但是动画呈现出来的效果比定时器更流畅,性能更好。

1、绘制游戏元素

// CSS
.table {
  position: relative;
  margin: 100px auto;
  width: 1080px;
  height: 596px;
  background: url(./台球桌.jpg) no-repeat;
  background-size: 100%;
}

.big {
  position: absolute;
  width: 1000px;
  height: 500px;
  left: 43px;
  top: 48px;
}

.box,
.box2 {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.5);
  position: absolute;
}

.box {
  background: radial-gradient(circle at 75% 30%, #fff 5px, #fffbfef1 8%, #aaaaaac4 60%, #faf6f9bd 100%);
}

.box2 {
  background: radial-gradient(circle at 75% 30%, #fff 5px, #ff21f4f1 8%, #d61d1dc4 60%, #ff219b 100%);
}

.big .box::before,
.box2::before {
  content: '';
  position: absolute;
  width: 100%;
  height: 100%;
  transform: scale(0.25) translate(-70%, -70%);
  background: radial-gradient(#fff, transparent);
  border-radius: 50%;
}

.gan {
  display: flex;
  height: 20px;
  position: absolute;
  left: 25px;
  top: 15px;
  transform-origin: 0 50%;
  transform: rotate(50deg);
  cursor: pointer;
}

.gan2 {
  width: 25px;
  height: 20px;
}

.gan3 {
  width: 375px;
  height: 20px;
  background: url(./Snipaste_2022-07-18_19-52-54.jpg) no-repeat center;
  background-size: 100%;
}
//html
<div class="table">
<div class="big">
  <div class="box">
    <div class="gan">
      <div class="gan2"></div>
      <div class="gan3"></div>
    </div>
  </div>
  <div class="box2"></div>
</div>
</div>
//JS
// 设置球的位置
//母球
const box1 = document.querySelector('.box')
box1.style.left = '300px'
box1.style.top = '150px'
//子球
const box2 = document.querySelector('.box2')
box2.style.left = '700px'
box2.style.top = '300px'
//球杆
const gan = document.querySelector('.gan')
const gan2 = document.querySelector('.gan2')
const gan3 = document.querySelector('.gan3')

2、球杆跟随鼠标旋转

先获取鼠标在页面的坐标,然后减去球心的坐标,就得到了一个相对坐标。然后把球心当成原点,计算出鼠标相对球心的角度,最后把这个角度赋值给球杆的transform属性,就可以实现球杆跟随鼠标旋转的效果了

//声明鼠标相对坐标变量
let x, y
// 获取鼠标的坐标,来计算球杆的角度
document.addEventListener('mousemove', function (e) {
  const position = box1.getBoundingClientRect()
  // 获取鼠标相对球心的坐标,因为盒子的position原点在左上角,所以要减去自身宽高的一半才是球心
  x = e.pageX - position.left - 25
  y = e.pageY - position.top - 25 - document.documentElement.scrollTop
  let z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)); // 勾股定理计算斜边值
  let cos = y / z;// 余弦
  let radian = Math.acos(cos);//用反三角函数求弧度
  let angle = 180 / (Math.PI / radian);//将弧度转换成角度
  if (x > 0 && y > 0) {//鼠标在第四象限
    angle = 90 - angle
  }
  if (x == 0 && y > 0) {//鼠标在y轴负方向上
    angle = 90;
  }
  if (x == 0 && y < 0) {//鼠标在y轴正方向上
    angle = 270;
  }
  if (x > 0 && y == 0) {//鼠标在x轴正方向上
    angle = 0;
  }
  if (x < 0 && y > 0) {//鼠标在第三象限
    angle = 90 + angle
  }
  if (x < 0 && y == 0) {//鼠标在x轴负方向
    angle = 180;
  }
  if (x < 0 && y < 0) {//鼠标在第二象限
    angle = 90 + angle
  }
  if (x > 0 && y < 0) {//鼠标在第一象限
    angle = 450 - angle
  }
  // 把计算出来的角度取模后赋值给球杆旋转角度
  gan.style.transform = `rotate(${angle % 360}deg)`
})

3、球杆的击球动画

球杆其实是由 3 个盒子组成的,最外面的大盒子来控制球杆的旋转,大盒子里面有两个盒子 gan2 和 gan3, gan3 这个盒子用来放球杆的图片。gan2 这个盒子是看不到的,它负责把球杆向外面撑开。所以球杆的动画就很简单了,只要增加和减少 gan2 盒子的宽,就能实现球杆的伸缩了。

实现动画就是用尾递归来重复调用 requestAnimationFrame 函数。

// // 球杆点击事件
document.querySelector('.gan3').addEventListener('click', function () {
  moveGan(gan2, 0)
})
// 球杆打击动画
function moveGan(item, num) {
  // i来控制函数的结束条件
  let i = num
  requestAnimationFrame(() {
    //获取元素的坐标值,要把字符串里的数字提取出来
    let moveX = parseFloat(item.style.width) || 25
    moveX += 15
    // 每一次调用这个函数,就让元素的宽+15px
    item.style.width = moveX + 'px'
    i++
    if (i >= 10) {
      // i>10时,就让球杆再缩回去
      return returnGan(item, 0)
    }
    // 使用尾递归来重复调用
    return moveGan(item, i)
  })
}
function returnGan(item, num) {
  let i = num
  requestAnimationFrame(() {
    let moveX = parseFloat(item.style.width) || 0
    moveX -= 15
    // 每一次调用这个函数,就让元素的宽-15px
    item.style.width = moveX + 'px'
    i++
    if (i >= 10) {
      return tick() //tick是击球的函数
    }
    return returnGan(item, i)
  })
}

4、球杆击球后,母球的移动

母球的击球动画同样是通过尾递归来重复调用 requestAnimationFrame 函数,但是涉及到墙壁反弹,以及撞击子秋,母球的移动函数的参数会复杂一点。

母球移动的速度和距离,是通过i这个变量来控制的,这个函数每调用一次,i 会递减。x 和 y 这两个参数会接收一个 -1 到 1 之间的值,起到一个方向系数的效果,通过参数把球杆的撞击方向传递进来。碰到边界之后,就把对应的系数取负,然后用新系数执行移动函数,就能起到反弹的效果了。

// 击打母球的函数
function tick() {
  // 通过绝对值判断打击角度,x和y就是鼠标相对球心的坐标
  if (Math.abs(x) > Math.abs(y)) {
    // 通过判断x,y是否大于0,判断打击方向
    if (x > 0 && y > 0 || x > 0 && y < 0) {
      raf(box1, -1, -1 / (x / y), 1000)
    } else {
      raf(box1, 1, 1 / (x / y), 1000)
    }
  } else {
    if (y > 0 && x > 0 || y > 0 && x < 0) {
      raf(box1, -1 / (y / x), -1, 1000)
    } else {
      raf(box1, 1 / (y / x), 1, 1000)
    }
  }
}

//..... 母球移动的函数里面还要加代码,所以这里就先不贴出来了。 

// 判断是否进洞的函数
function test(x, y) {
  if (x < 10 && y < 10 || x > 940 && y < 10 || x > 940 && y > 440 || x < 10 && y > 440
    || x > 475 && x < 525 && y < 5 || x > 475 && x < 525 && y > 445) {
    return true
  }
}

5、母球撞击子球移动

这是最麻烦的一步,撞击后两个球的运动轨迹都会发生变化。只考虑最普通的撞击,子球的运动方向应该是撞击点与子球球心这条直线的方向,这个比较好计算。母球的撞击后的方向应该是以撞击点的那条切线进行反弹,三角函数几乎忘光了,这个我也不知道怎么计算了,所以用了个简易的算法,就和撞墙壁一样直接反弹,这样会导致某些角度下,母球撞击之后的方向不正常。

把这个撞击判断加到母球移动的函数里面,然后再补充一个子球的移动函数,整个代码就写完了

//母球移动
// 获取坐标,要把字符串里的数字提取出来
let fx = parseFloat(box1.style.left)
let fy = parseFloat(box1.style.top)
let gx = parseFloat(box2.style.left)
let gy = parseFloat(box2.style.top)
// 声明用判断撞球角度的变量
let n
// 控制子球移动函数的调用
let p = true
function raf(item, x, y, num) {
  //击球后隐藏球杆
  gan3.style.display = 'none'
  // item是目标元素,x和y对应移动方向的系数,i用来控制移动速度
  let i = num
  requestAnimationFrame(() {
    fx += x * 5 * i / 500
    fy += y * 5 * i / 500
    item.style.left = fx + 'px'
    item.style.top = fy + 'px'
    i -= 2
    // 边界判断,球桌宽1000高500,球宽高50,所以边界就是0-950
    if (fx > 950) { // 右边界,让x系数反过来
      fx = 950
      return raf(item, -x, y, i)
    } else if (fy > 450) { // 下边界,让y系数反过来
      fy = 450
      return raf(item, x, -y, i)
    } else if (fx < 0) { // 左边界,让x系数反过来
      fx = 0
      return raf(item, -x, y, i)
    } else if (fy < 0) { // 上边界,让y系数反过来
      fy = 0
      return raf(item, x, -y, i)
    }
    // i<=50就停止移动,然后显示球杆
    if (i <= 50) return gan3.style.display = 'block'
    // 判断球是否进洞
    if (test(fx, fy)) {
      return item.style.display = 'none'
    }
    //两个球撞击时的判断
    if (fx < gx + 50 && fx > gx - 50 && fy < gy + 50 && fy > gy - 50) {
      // 子球前进的角度,就是撞击时,两个圆心连线的夹角
      n = Math.abs(gx - fx) >= Math.abs(gy - fy) ? Math.abs(gx - fx) : Math.abs(gy - fy)
      // n用来控制调用函数时x,y的大小,不能大于1,否则移动速度会异常
      if (p) raf2(box2, (gx - fx) / n, (gy - fy) / n, i)
      // 只有第一次碰撞时,会调用一次子球移动的函数,避免一次击球产生多次撞击时,这个函数被多次调用
      p = false
      return raf(item, -x, y, i)
    }
    return raf(item, x, y, i)
  })
}
 //子球移动
function raf2(item, x, y, num) {
  let i = num
  requestAnimationFrame(() {
    //获取元素的坐标值,要把字符串里的数字提取出来
    gx += x * 5 * i / 700
    gy += y * 5 * i / 700
    item.style.left = gx + 'px'
    item.style.top = gy + 'px'
    i -= 2
    if (gx > 950) {
      gx = 950
      return raf2(item, -x, y, i)
    } else if (gy > 450) {
      gy = 450
      return raf2(item, x, -y, i)
    } else if (gx < 0) {
      gx = 0
      return raf2(item, -x, y, i)
    } else if (gy < 0) {
      gy = 0
      return raf2(item, x, -y, i)
    }
    //两个球触碰判断
    if (fx < gx + 50 && fx > gx - 50 && fy < gy + 50 && fy > gy - 50) {
      return raf2(box2, (gx - fx) / n, (gy - fy) / n, i)
    }
    if (i <= 50) return p = true // 移动函数执行完后,重置p这个变量
    // 判断球是否进洞
    if (test(gx, gy)) {
      return item.style.display = 'none'
    }
    return raf2(item, x, y, i)
  })
}
图片
图片

这个小游戏实现的并不完美,因为用到了太多的递归,很多细节方面不好控制,球的运动轨迹也很难计算,在某些角度下会出现BUG。球虽然是圆的,但是它的盒子是正方形,所以撞击有的时候会看着很奇怪。移动的函数写的也有缺陷,它不能复用,如果想添加多个球,函数就得改。

这个破产版的台球主要就是写着玩一玩,尝试了一下JS动画的实现 , 不喜勿喷。

责任编辑:姜华 来源: 前端YUE

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK