当前位置: 首页 > 科技观察

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

时间:2023-03-17 13:34:26 科技观察

前言突然想用JS写一个台球的小游戏。一波三折,实现了一个简单的版本。用到的知识主要是通过递归调用requestAnimationFrame,以及一些简单的三角函数角度计算。requestAnimationFrame是一个JS动画框架,简单来说类似于定时器,但是动画比定时器更流畅,性能也更好。1、绘制游戏元素CSS//CSS.table{position:relative;边距:100px自动;宽度:1080px;高度:596px;背景:url(./台球表.jpg)不重复;背景大小:100%;}.big{位置:绝对;宽度:1000px;高度:500px;左:43px;top:48px;}.box,.box2{width:50px;高度:50px;边界半径:50%;框阴影:插入0010pxrgba(255,255,255,0.5);position:absolute;}.box{background:radial-gradient(circleat75%30%,#fff5px,#fffbfef18%,#aaaaaac460%,#faf6f9bd100%);}.box2{background:radial-gradient(圈在75%30%,#fff5px,#ff21f4f18%,#d61d1dc460%,#ff219b100%);}.big.box::before,.box2::before{content:'';位置:绝对;宽度:100%;高度:100%;转换:比例(0.25)翻译(-70%,-70%);背景:径向渐变(#fff,透明);border-radius:50%;}.gan{显示:flex;高度:20px;位置:绝对;左:25px;顶部:15px;变换原点:050%;变换:旋转(50度);光标:指针;}.gan2{宽度:25px;高度:20px;}.gan3{宽度:375px;高度:20px;背景:url(./Snipaste_2022-07-18_19-52-54.jpg)无重复中心;背景大小:100%;}html//html

JS//JS//设置球的位置//Cueballconstbox1=document.querySelector('.box')box1.style.left='300px'box1.style.top='150px'//子球constbox2=document.querySelector('.box2')box2.style.left='700px'box2.style.top='300px'//clubconstgan=document.querySelector('.gan')constgan2=document.querySelector('.gan2')constgan3=document.querySelector('.gan3')2.球杆随鼠标旋转,先获取鼠标在页面上的坐标,再减去球心坐标,得到相对坐标然后以球心为原点,计算鼠标相对于球心的角度,最后将这个角度赋值给球杆的transform属性,球杆随鼠标旋转的效果就可以了realized//声明鼠标的相对坐标变量letx,y//获取鼠标坐标计算cue的角度document.addEventListener('mousemove',function(e){constposition=box1.getBoundingClientRect()//获取鼠标相对于小球中心的坐标,因为盒子的位置原点在左上角,所以减去自身宽高的一半得到小球中心x=e.pageX-position.left-25y=e.pageY-position.top-25-document.documentElement.scrollTop让z=Math.sqrt(Math.pow(x,2)+Math.pow(y,2));//勾股定理计算斜边值letcos=y/z;//余弦letradian=Math.acos(cos);//使用反三角函数ons求弧度letangle=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轴角度=180;}如果(x<0&&a议员;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盒子是用来放cue的图片的。gan2盒子是看不见的。它负责将提示向外转动。打开。所以cue的动画很简单,只要增加或减小gan2盒子的宽度,就可以实现cue的展开和收缩。动画的实现是使用尾递归反复调用requestAnimationFrame函数。////俱乐部点击事件document.querySelector('.gan3').addEventListener('click',function(){moveGan(gan2,0)})//俱乐部罢工动画functionmoveGan(item,num){//i用于控制函数的结束条件leti=numrequestAnimationFrame((){//获取元素的坐标值并从字符串中提取数字letmoveX=parseFloat(item.style.width)||25moveX+=15//每次调用这个函数,让元素的宽度+15pxitem.style.width=moveX+'px'i++if(i>=10){//当i>10时,让球杆再次收回returnreturnGan(item,0)}//使用尾递归重复调用returnmoveGan(item,i)})}functionreturnGan(item,num){leti=numrequestAnimationFrame((){letmoveX=parseFloat(item.style.width)||0moveX-=15//每次调用这个函数,元素的宽度为-15pxitem.style.width=moveX+'px'i++if(i>=10){returntick()//tick是击球的函数}returnreturnGan(item,i)})}4.母球击球后,母球的运动动画也通过尾递归反复调用requestAnimationFrame函数。但是涉及到弹墙打子秋,母球运动函数的参数会稍微复杂一点。母球的移动速度和距离由变量i控制。每次调用此函数时,我都会减少。x和y这两个参数会收到一个介于-1和1之间的值,作为方向系数,通过参数传入球杆的撞击方向。撞到边界后,取相应的系数为负,然后用新的系数执行运动函数,就可以达到反弹的效果。//击打母球的函数functiontick(){//通过绝对值判断击打角度,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{如果(y>0&&x>0||y>0&&x<0){raf(box1,-1/(y/x),-1,1000)}else{raf(box1,1/(y/x),1,1000)}}}//.....添加到母球运动功能代码,所以我不会在这里发布。//判断是否入洞函数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){returntrue}}5.母球击中子球并移动。这是最麻烦的一步。后两个球的轨迹会发生变化。只考虑最常见的撞击,子球的运动方向应该是撞击点到子球中心的直线方向,这样比较容易计算。击球后母球的方向应该是击球点弹起的切线。我差点忘了三角函数。这个我也不知道怎么算,所以用了个简单的算法,直接像撞墙一样反弹,这将导致母球在某些角度撞击后的方向不正常。将这个撞击判断添加到母球运动函数中,再添加一个子球运动函数,整个代码就完成了//Cueballmovement//获取坐标,提取字符串中的数字letfx=parseFloat(box1.style.left)letfy=parseFloat(box1.style.top)letgx=parseFloat(box2.style.left)letgy=parseFloat(box2.style.top)//声明用于判断台球角度的变量ballletn//控制球运动函数的调用letp=truefunctionraf(item,x,y,num){//击球后隐藏球杆gan3.style.display='none'//itemis目标元素,x和y对应运动方向的系数,i用于控制运动速度leti=numrequestAnimationFrame((){fx+=x*5*i/500fy+=y*5*i/500item.style.left=fx+'px'item.style.top=fy+'px'i-=2//边界判断,表宽1000高500,球宽高50,所以边界是0-950if(fx>950){//右边界,反向x系数fx=950returnraf(item,-x,y,i)}elseif(fy>450){//下边界,反向y系数fy=450returnraf(item,x,-y,i)}elseif(fx<0){//左边界,让x系数反转fx=0returnraf(item,-x,y,i)}elseif(fy<0){//上边界,让y系数反转fy=0returnraf(item,x,-y,i)}//当i<=50时停止移动,然后显示球杆if(i<=50)returngan3.style.display='block'//判断球是否进洞if(test(fx,fy)){returnitem.style.display='none'}//两个球碰撞时判断if(fxgx-50&&fygy-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)//只有第一次碰撞发生时,才会调用一次子球的移动函数,避免一个造成多次撞击shot,这个函数被调用了多次p=falsereturnraf(item,-x,y,i)}returnraf(item,x,y,i)})}//球运动函数raf2(item,x,y,num){leti=numrequestAnimationFrame((){//获取元素的坐标值,从字符串中提取数字gx+=x*5*i/700gy+=y*5*i/700item.style.left=gx+'px'item.style.top=gy+'px'i-=2if(gx>950){gx=950returnraf2(item,-x,y,i)}else如果(gy>450){gy=450返回raf2(item,x,-y,i)}elseif(gx<0){gx=0returnraf2(item,-x,y,i)}elseif(gy<0){gy=0returnraf2(item,x,-y,i)}//两个球接触判断if(fxgx-50&&fygy-50){returnraf2(box2,(gx-fx)/n,(gy-fy)/n,i)}if(i<=50)returnp=true//移动函数执行完毕后,重置变量p//判断小球是否进入holeif(test(gx,gy)){returnitem.style.display='none'}returnraf2(item,x,y,i)})}总结这个小游戏并不完美,因为它使用了太多的递归,很多细节不好控制,球的轨迹也很难计算,在某些角度会有bug。虽然球是圆的,但它的盒子是方的,所以撞击有时看起来很奇怪。.移动功能也有缺陷,不能重复使用,如果要添加更多的球,则必须更改功能。这个破产版的台球主要是为了好玩而写的,我尝试了JS动画的实现,不喜勿喷。