最近刷了一部電影《動物世界》,感概原來簡單的「剪刀石頭布」遊戲還能夠這麼燒腦,強大的數據分析能力、對人性心理的靈敏嗅覺等。看完以後饒有興致,因而便利用socket技術,實現了一個「動物世界」多人對戰系統。前端
故事講述的是男主被髮小欺騙,欠下了一屁股債,爲了償還債務被迫上了一艘賊船,同時上船的還有一批人,大夥兒的情況都差很少。上船的好處是有機會還清債務而且還可能得到一筆巨大的財富,這對於在現實世界中已經生活不能自理的人來講,無疑是一次改變人生的機會,而一旦失敗的話,就要被拉去作人體實驗(恐怖如斯)。vue
上船的人會進行一場賭博,就是咱們小時候常玩的「石頭剪刀布」,每人初始時擁有12張卡牌,石頭、剪刀、布各4張,而且擁有3顆星,你們能夠找任何一我的做爲對手,每人各出一張卡牌,獲勝者將從失敗方拿走一顆星。遊戲獲勝條件是手裏卡牌所有消耗完而且擁有的星星很多於3顆,反之,若卡牌消耗完且星星少於3顆、或還有卡牌但星星爲0,都視爲失敗。node
咱們將利用koa
來搭建一個socket服務器,來管理客戶端的消息接受和分發。git
主要邏輯:github
const io = SocketIO(server) // 創建socket鏈接 const users = {} // 緩存當前連接的用戶 const challengeData = {} // 緩存用戶發起的對戰信息 io.on('connection', socket => { // 客戶端鏈接後 const id = socket.id // 當前鏈接的unique標識 socket.emit('connected') // 告訴客戶端已經鏈接成功 // 接收客戶端的open事件 socket.on('open', name => { // 初始化數據 users[id] = { id, name, // 用戶暱稱 star: 3, // 用戶擁有的星星 stone: 4, // 用戶擁有的「石頭」卡牌數量 scissors: 4, // 用戶擁有的「剪刀」卡牌數量 paper: 4 // 用戶擁有的「布」卡牌數量 } // 通知全部人,當前全部用戶的信息 io.emit('update_users', users) }) // 用戶發起挑戰 socket.on('challenge', data => { // data包括fromCard(發起者出示的卡牌)、toId(被挑戰者的id) data.fromId = id challengeData[id] = data io.to(data.toId).emit('accept_challenge', users[id]) // 告訴對方有人要和你對戰 }) // 發起者取消了挑戰 socket.on('cancel_challenge', () => { io.to(challengeData[id].toId).emit('cancel_challenge') // 告訴對方挑戰已取消 delete challengeData[id] // 刪除緩存的數據 }) // 對方接受挑戰的信息 socket.on('respond_challenge', data => { if (data.accept) { // 接受 let cd = challengeData[data.fromId] cd.toCard = data.toCard // 被挑戰者出示的卡牌 // 雙方卡牌各減小1 users[cd.fromId][cd.fromCard]-- users[cd.toId][cd.toCard]-- let result = getChallengeResult(cd.fromCard, cd.toCard) // 得到挑戰結果 // 比賽後的星星變動 if (result === 1) { users[cd.fromId].star++ users[cd.toId].star-- } else if (result === -1) { users[cd.fromId].star-- users[cd.toId].star++ } // 告訴挑戰者和被挑戰者,比賽的結果 io.to(cd.fromId).emit('result_challenge', result, users) io.to(cd.toId).emit('result_challenge', -result, users) } else { // 拒絕 io.to(data.fromId).emit('cancel_challenge') // 告訴發起者對方不接受挑戰 } delete challengeData[data.fromId] }) // 比賽勝利 socket.on('success_challenge', () => { // 告訴全部人,有人得到了勝利 socket.broadcast.emit('success_challenge', users[id]) }) // 斷開鏈接 socket.on('disconnect', () => { delete users[id] // 廣播用戶已退出 socket.broadcast.emit('update_users', users) }) })
前端使用Vue
來進行頁面渲染。後端
import request from '@/common/request' import tips from '@axe/tips' import modal from '@axe/modal' import Loading from './components/Loading.vue' /* eslint-disable no-alert */ export default { name: 'App', components: { Loading }, data () { return { isConnected: false, id: '', selectedUserId: '', selectedCard: '', users: {}, acceptChallenge: false }; }, computed: { userInfo () { let user = this.users[this.id] || {} return { star: user.star || 0, stone: user.stone || 0, scissors: user.scissors || 0, paper: user.paper || 0 } }, totalInfo () { let info = { stone: 0, scissors: 0, paper: 0 } for (let id in this.users) { let user = this.users[id] info.stone += user.stone || 0 info.scissors += user.scissors || 0 info.paper += user.paper || 0 } return info } }, methods: { handleSelectCard (type) { this.selectedCard = type }, handleSelectUser (id) { if (this.id === id) { tips.show({ content: '不能夠挑戰本身哦' }) return } this.selectedUserId = id }, handleChallenge () { if (this.gameover) { tips.show({ content: '遊戲已結束,請從新開始' }) return } if (!this.selectedCard) { tips.show({ content: '請挑選卡牌' }) return } if (this.users[this.id][this.selectedCard] <= 0) { tips.show({ content: '這類卡牌已耗盡' }) return } let user = this.users[this.selectedUserId] if (!user) { tips.show({ content: '請挑選對手' }) return } if (user.star <= 0 || (user.stone + user.scissors + user.paper) <= 0) { tips.show({ content: '該用戶已不具有對戰能力了' }) return } if (!this.acceptChallenge) { this.socket.emit('challenge', { fromCard: this.selectedCard, toId: this.selectedUserId }) modal.show({ title: '發起挑戰', content: '等待對方接受中...', confirmText: '取消挑戰' }, t => { if (t === 'confirm') { this.socket.emit('cancel_challenge') } }) } else { this.socket.emit('respond_challenge', { accept: true, fromId: this.challengeFromUser.id, toCard: this.selectedCard }) // 重置記錄 this.acceptChallenge = false } } }, mounted () { request({ url: '/api/info' }).then(data => { // 使用ip創建鏈接,局域網內其餘設備也能夠訪問 this.socket = window.io.connect('http://' + data.ip + ':' + data.port) this.socket.on('connected', () => { let name = window.prompt('請輸入您優雅高貴的稱呼') if (!name || !name.trim()) { name = this.socket.id } // 告訴服務器,有人進來了 this.socket.emit('open', name) // 已鏈接 this.id = this.socket.id this.isConnected = true }) this.socket.on('update_users', users => { this.users = users }) // 是否接受挑戰 this.socket.on('accept_challenge', fromUser => { modal.show({ title: '接受挑戰', content: '是否接受來自【' + fromUser.name + '】的挑戰?', confirmText: '接受', cancelText: '拒絕' }, t => { if (t === 'confirm') { // 緩存挑戰信息,等待用戶選擇出示的卡牌 this.selectedUserId = fromUser.id this.acceptChallenge = true this.challengeFromUser = fromUser } else { this.socket.emit('respond_challenge', { accept: false, fromId: fromUser.id }) } }) }) this.socket.on('cancel_challenge', () => { this.acceptChallenge = false modal.hide() tips.show({ content: '對方取消了挑戰' }) }) // 監聽對戰結果 this.socket.on('result_challenge', (result, users) => { this.users = users modal.hide() tips.show({ content: result === 0 ? '平局' : (result === 1 ? '你贏了' : '你輸了') }, () => { // 檢測遊戲勝利和失敗條件 let user = users[this.id] let cardCount = user.stone + user.scissors + user.paper if (user.star >= 3 && cardCount <= 0) { this.socket.emit('success_challenge') modal.show({ title: '遊戲勝利', content: '恭喜你得到了勝利!', confirmText: '再來一局' }, t => { if (t === 'confirm') { window.location.reload() } }) } else if (user.star <= 0 || cardCount <= 0) { this.gameover = true modal.show({ title: '遊戲結束', content: user.star <= 0 ? '你已經沒有星星了' : '你已經沒有卡牌了', confirmText: '從新開始' }, t => { if (t === 'confirm') { window.location.reload() } }) } }) }) // 接收系統廣播,有人挑戰成功的信息 this.socket.on('success_challenge', user => { window.alert(`恭喜【${user.name}】挑戰成功,戰績(${user.star})顆星`) }) }) } }
遊戲預覽api