新聞中心
前言
突發(fā)奇想想用JS寫一個(gè)臺(tái)球小游戲,磕磕碰碰之后,算是實(shí)現(xiàn)了一個(gè)簡易版的。用到的知識(shí)主要是通過遞歸來調(diào)用requestAnimationFrame,以及一些簡單的三角函數(shù)角度計(jì)算。requestAnimationFrame就是一個(gè)JS動(dòng)畫幀,簡單來說和定時(shí)器有點(diǎn)相似,但是動(dòng)畫呈現(xiàn)出來的效果比定時(shí)器更流暢,性能更好。

創(chuàng)新互聯(lián)服務(wù)項(xiàng)目包括雙河網(wǎng)站建設(shè)、雙河網(wǎng)站制作、雙河網(wǎng)頁制作以及雙河網(wǎng)絡(luò)營銷策劃等。多年來,我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,雙河網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到雙河省份的部分城市,未來相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
1、繪制游戲元素
CSS
// CSS
.table {
position: relative;
margin: 100px auto;
width: 1080px;
height: 596px;
background: url(./臺(tái)球桌.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
//html
JS
//JS
// 設(shè)置球的位置
//母球
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、球桿跟隨鼠標(biāo)旋轉(zhuǎn)
先獲取鼠標(biāo)在頁面的坐標(biāo),然后減去球心的坐標(biāo),就得到了一個(gè)相對(duì)坐標(biāo)。然后把球心當(dāng)成原點(diǎn),計(jì)算出鼠標(biāo)相對(duì)球心的角度,最后把這個(gè)角度賦值給球桿的transform屬性,就可以實(shí)現(xiàn)球桿跟隨鼠標(biāo)旋轉(zhuǎn)的效果了
//聲明鼠標(biāo)相對(duì)坐標(biāo)變量
let x, y
// 獲取鼠標(biāo)的坐標(biāo),來計(jì)算球桿的角度
document.addEventListener('mousemove', function (e) {
const position = box1.getBoundingClientRect()
// 獲取鼠標(biāo)相對(duì)球心的坐標(biāo),因?yàn)楹凶拥膒osition原點(diǎn)在左上角,所以要減去自身寬高的一半才是球心
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)); // 勾股定理計(jì)算斜邊值
let cos = y / z;// 余弦
let radian = Math.acos(cos);//用反三角函數(shù)求弧度
let angle = 180 / (Math.PI / radian);//將弧度轉(zhuǎn)換成角度
if (x > 0 && y > 0) {//鼠標(biāo)在第四象限
angle = 90 - angle
}
if (x == 0 && y > 0) {//鼠標(biāo)在y軸負(fù)方向上
angle = 90;
}
if (x == 0 && y < 0) {//鼠標(biāo)在y軸正方向上
angle = 270;
}
if (x > 0 && y == 0) {//鼠標(biāo)在x軸正方向上
angle = 0;
}
if (x < 0 && y > 0) {//鼠標(biāo)在第三象限
angle = 90 + angle
}
if (x < 0 && y == 0) {//鼠標(biāo)在x軸負(fù)方向
angle = 180;
}
if (x < 0 && y < 0) {//鼠標(biāo)在第二象限
angle = 90 + angle
}
if (x > 0 && y < 0) {//鼠標(biāo)在第一象限
angle = 450 - angle
}
// 把計(jì)算出來的角度取模后賦值給球桿旋轉(zhuǎn)角度
gan.style.transform = `rotate(${angle % 360}deg)`
})
3、球桿的擊球動(dòng)畫
球桿其實(shí)是由 3 個(gè)盒子組成的,最外面的大盒子來控制球桿的旋轉(zhuǎn),大盒子里面有兩個(gè)盒子 gan2 和 gan3, gan3 這個(gè)盒子用來放球桿的圖片。gan2 這個(gè)盒子是看不到的,它負(fù)責(zé)把球桿向外面撐開。所以球桿的動(dòng)畫就很簡單了,只要增加和減少 gan2 盒子的寬,就能實(shí)現(xiàn)球桿的伸縮了。
實(shí)現(xiàn)動(dòng)畫就是用尾遞歸來重復(fù)調(diào)用 requestAnimationFrame 函數(shù)。
// // 球桿點(diǎn)擊事件
document.querySelector('.gan3').addEventListener('click', function () {
moveGan(gan2, 0)
})
// 球桿打擊動(dòng)畫
function moveGan(item, num) {
// i來控制函數(shù)的結(jié)束條件
let i = num
requestAnimationFrame(() {
//獲取元素的坐標(biāo)值,要把字符串里的數(shù)字提取出來
let moveX = parseFloat(item.style.width) || 25
moveX += 15
// 每一次調(diào)用這個(gè)函數(shù),就讓元素的寬+15px
item.style.width = moveX + 'px'
i++
if (i >= 10) {
// i>10時(shí),就讓球桿再縮回去
return returnGan(item, 0)
}
// 使用尾遞歸來重復(fù)調(diào)用
return moveGan(item, i)
})
}
function returnGan(item, num) {
let i = num
requestAnimationFrame(() {
let moveX = parseFloat(item.style.width) || 0
moveX -= 15
// 每一次調(diào)用這個(gè)函數(shù),就讓元素的寬-15px
item.style.width = moveX + 'px'
i++
if (i >= 10) {
return tick() //tick是擊球的函數(shù)
}
return returnGan(item, i)
})
}
4、球桿擊球后,母球的移動(dòng)
母球的擊球動(dòng)畫同樣是通過尾遞歸來重復(fù)調(diào)用 requestAnimationFrame 函數(shù),但是涉及到墻壁反彈,以及撞擊子秋,母球的移動(dòng)函數(shù)的參數(shù)會(huì)復(fù)雜一點(diǎn)。
母球移動(dòng)的速度和距離,是通過i這個(gè)變量來控制的,這個(gè)函數(shù)每調(diào)用一次,i 會(huì)遞減。x 和 y 這兩個(gè)參數(shù)會(huì)接收一個(gè) -1 到 1 之間的值,起到一個(gè)方向系數(shù)的效果,通過參數(shù)把球桿的撞擊方向傳遞進(jìn)來。碰到邊界之后,就把對(duì)應(yīng)的系數(shù)取負(fù),然后用新系數(shù)執(zhí)行移動(dòng)函數(shù),就能起到反彈的效果了。
// 擊打母球的函數(shù)
function tick() {
// 通過絕對(duì)值判斷打擊角度,x和y就是鼠標(biāo)相對(duì)球心的坐標(biāo)
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)
}
}
}
//..... 母球移動(dòng)的函數(shù)里面還要加代碼,所以這里就先不貼出來了。
// 判斷是否進(jìn)洞的函數(shù)
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、母球撞擊子球移動(dòng)
這是最麻煩的一步,撞擊后兩個(gè)球的運(yùn)動(dòng)軌跡都會(huì)發(fā)生變化。只考慮最普通的撞擊,子球的運(yùn)動(dòng)方向應(yīng)該是撞擊點(diǎn)與子球球心這條直線的方向,這個(gè)比較好計(jì)算。母球的撞擊后的方向應(yīng)該是以撞擊點(diǎn)的那條切線進(jìn)行反彈,三角函數(shù)幾乎忘光了,這個(gè)我也不知道怎么計(jì)算了,所以用了個(gè)簡易的算法,就和撞墻壁一樣直接反彈,這樣會(huì)導(dǎo)致某些角度下,母球撞擊之后的方向不正常。
把這個(gè)撞擊判斷加到母球移動(dòng)的函數(shù)里面,然后再補(bǔ)充一個(gè)子球的移動(dòng)函數(shù),整個(gè)代碼就寫完了
//母球移動(dòng)
// 獲取坐標(biāo),要把字符串里的數(shù)字提取出來
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
// 控制子球移動(dòng)函數(shù)的調(diào)用
let p = true
function raf(item, x, y, num) {
//擊球后隱藏球桿
gan3.style.display = 'none'
// item是目標(biāo)元素,x和y對(duì)應(yīng)移動(dòng)方向的系數(shù),i用來控制移動(dòng)速度
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,球?qū)捀?0,所以邊界就是0-950
if (fx > 950) { // 右邊界,讓x系數(shù)反過來
fx = 950
return raf(item, -x, y, i)
} else if (fy > 450) { // 下邊界,讓y系數(shù)反過來
fy = 450
return raf(item, x, -y, i)
} else if (fx < 0) { // 左邊界,讓x系數(shù)反過來
fx = 0
return raf(item, -x, y, i)
} else if (fy < 0) { // 上邊界,讓y系數(shù)反過來
fy = 0
return raf(item, x, -y, i)
}
// i<=50就停止移動(dòng),然后顯示球桿
if (i <= 50) return gan3.style.display = 'block'
// 判斷球是否進(jìn)洞
if (test(fx, fy)) {
return item.style.display = 'none'
}
//兩個(gè)球撞擊時(shí)的判斷
if (fx < gx + 50 && fx > gx - 50 && fy < gy + 50 && fy > gy - 50) {
// 子球前進(jìn)的角度,就是撞擊時(shí),兩個(gè)圓心連線的夾角
n = Math.abs(gx - fx) >= Math.abs(gy - fy) ? Math.abs(gx - fx) : Math.abs(gy - fy)
// n用來控制調(diào)用函數(shù)時(shí)x,y的大小,不能大于1,否則移動(dòng)速度會(huì)異常
if (p) raf2(box2, (gx - fx) / n, (gy - fy) / n, i)
// 只有第一次碰撞時(shí),會(huì)調(diào)用一次子球移動(dòng)的函數(shù),避免一次擊球產(chǎn)生多次撞擊時(shí),這個(gè)函數(shù)被多次調(diào)用
p = false
return raf(item, -x, y, i)
}
return raf(item, x, y, i)
})
}
//子球移動(dòng)
function raf2(item, x, y, num) {
let i = num
requestAnimationFrame(() {
//獲取元素的坐標(biāo)值,要把字符串里的數(shù)字提取出來
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)
}
//兩個(gè)球觸碰判斷
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 // 移動(dòng)函數(shù)執(zhí)行完后,重置p這個(gè)變量
// 判斷球是否進(jìn)洞
if (test(gx, gy)) {
return item.style.display = 'none'
}
return raf2(item, x, y, i)
})
}
總結(jié)
這個(gè)小游戲?qū)崿F(xiàn)的并不完美,因?yàn)橛玫搅颂嗟倪f歸,很多細(xì)節(jié)方面不好控制,球的運(yùn)動(dòng)軌跡也很難計(jì)算,在某些角度下會(huì)出現(xiàn)BUG。球雖然是圓的,但是它的盒子是正方形,所以撞擊有的時(shí)候會(huì)看著很奇怪。移動(dòng)的函數(shù)寫的也有缺陷,它不能復(fù)用,如果想添加多個(gè)球,函數(shù)就得改。
這個(gè)破產(chǎn)版的臺(tái)球主要就是寫著玩一玩,嘗試了一下JS動(dòng)畫的實(shí)現(xiàn) , 不喜勿噴。
本文題目:用原生 JS 寫一個(gè)簡易版的臺(tái)球
標(biāo)題路徑:http://fisionsoft.com.cn/article/cdiicij.html


咨詢
建站咨詢
