The core of the realization of battle lies in the application of Watch. The cloud development data set is based on the encapsulation of webSocket to monitor the update event of the data that meets the query conditions in the set. When the monitored DOC data changes, the onChange event callback is triggered, and the corresponding scene is switched through the callback data. That is, monitor the current room records, when both users choose words, the corresponding business can be displayed
Words every day bucket – friends versus specific implementation
Create a room (owner)
Friends vs. Friends, we start with the house owner creating a room, and users create a new room by clicking the event of the friend vs. button on the home page.
Events trigger
<button open-type="getUserInfo" bindgetuserinfo="onChallengeFriend">Friends vs. Friends</button>
Copy the code
onChallengeFriend: throttle(async function(e) { // Click to get user information, update the information, and create a room
const { detail: { userInfo } } = e
if (userInfo) {
await userModel.updateInfo(userInfo) // Update user information
this.triggerEvent('onChallengeFriend') // Triggers the create room operation of the parent component
} else {
this.selectComponent('#authFailMessage').show() // Authorization failed, prompting the user to authorize}},1000),
Copy the code
Function throttling is used to optimize the experience, as a knowledge point. When the user clicks the create room button many times quickly, we want it to trigger once if it is triggered many times within 1s, so we use throttle to package it as follows:
export function throttle(fn, gapTime = 500) {
let _lastTime = null
return function() {
const _nowTime = +new Date(a)if(_nowTime - _lastTime > gapTime || ! _lastTime) { fn.apply(this.arguments) // Call (fn); // Call (fn); call (fn)
_lastTime = _nowTime
}
}
}
Copy the code
The complete process of creating a room
Creating a room is divided into four small steps. The code below is the complete process
/** * Create word PK room * 1 when no room is available. Get the number of words in the match, and take the random words of the corresponding class * 2. Format the word list * 3. Create room (write room information to database store) * 4. Jump to the battle room page */
async createCombatRoom(isFriend = true) {
try {
$.loading('Generate random words in... ')
const { data: { userInfo: { bookId, bookDesc, bookName } } } = this
// 1. Get the number of matched words and random words of the corresponding class
const number = getCombatSubjectNumber()
const { list: randomList } = await wordModel.getRandomWords(bookId, number * SUBJECT_HAS_OPTIONS_NUMBER)
// 2. Format the word list
const wordList = formatList(randomList, SUBJECT_HAS_OPTIONS_NUMBER)
$.loading('Create room... ')
// create room (write room information to database)
const roomId = await roomModel.create(wordList, isFriend, bookDesc, bookName)
$.hideLoading()
// 4. Jump to the battle room page
router.push('combat', { roomId })
} catch (error) {
$.hideLoading()
this.selectComponent('#createRoomFail').show()
}
},
Copy the code
Details: Random words taken out
First get the configuration item for how many words the room is to create
/** * Get the number of words per game, from localStorage, users can modify the number of words per game in the Settings */
export const getCombatSubjectNumber = function() {
const number = $.storage.get(SUBJECT_NUMBER) // Read the data in the cache
if (typeofnumber ! = ='number'| |! PK_SUBJECTS_NUMBER.includes(number)) {// If the read data is invalid, use the default data
setCombatSubjectNumber(DEFAULT_PK_SUBJECT_NUMBER) // Use the default data to modify the original data in the cache
return DEFAULT_PK_SUBJECT_NUMBER
}
return number
}
Copy the code
The number of words to be retrieved is equal to the number configured in the cache * the number of options
The number * SUBJECT_HAS_OPTIONS_NUMBER, for example, will be 10(the current room requires 10 matches) * 4(each subject has 4 options) = 40 words, and then all the words will be randomly combined to generate a list of the words to be played
// const number = getCombatSubjectNumber()
// const { list: randomList } = await wordModel.getRandomWords(bookId, number * SUBJECT_HAS_OPTIONS_NUMBER)
import Base from './base'
import $ from '. /.. /utils/Tool'
import log from '. /.. /utils/log'
const collectionName = 'word'
/** * permissions: all users can read */
class WordModel extends Base {
constructor() {
super(collectionName)
}
getRandomWords(bookId, size) {
const where = bookId === 'random' ? {} : { bookId }
try {
return this.model.aggregate()
.match(where)
.limit(999999)
.sample({ size }) // Select random data from sample
.end()
} catch (error) {
log.error(error)
throw error
}
}
}
export default new WordModel()
Copy the code
Details: Combine to generate a list of battle words
// const wordList = formatList(randomList, SUBJECT_HAS_OPTIONS_NUMBER)
@param {Array} list of random words * @param {Number} len How many options per topic */
export const formatList = (list, len) = > {
const lists = chunk(list, len)
return lists.map(option= > {
const obj = { options: []}const randomIndex = Math.floor(Math.random() * len)
option.forEach((word, index) = > {
if (index === randomIndex) {
obj['correctIndex'] = randomIndex
obj['word'] = word.word
obj['wordId'] = word._id
obj['usphone'] = word.usphone
}
const { pos, tranCn } = word.trans.sort((a)= > Math.random() - 0.5) [0]
let trans = tranCn
if (pos) {
trans = `${pos}.${tranCn}`
}
obj.options.push(trans)
})
return obj
})
}
Copy the code
The combined data format is shown as follows:
Details: Formally create room, write to database
// const roomId = await roomModel.create(wordList, isFriend, bookDesc, bookName)
// model/room.js (encapsulate room data set operations)
// ...
async create(list, isFriend, bookDesc, bookName) {
try {
const { _id = ' ' } = await this.model.add({ data: {
list, // Generate a list of battle words
isFriend, // Whether to play as friends
createTime: this.date,
bookDesc, // The description of the battle word book
bookName, // Fight the word book name
left: { // The owner's information
openid: '{openid}'./ / homeowners openid
gradeSum: 0.// Total score of the owner versus
grades: {} // Match score and selected index for each title
},
right: { // Information about invited users
openid: ' './ / users openid
gradeSum: 0.// Total user score
grades: {} // Match score and selected index for each title
},
state: ROOM_STATE.IS_OK, // The room is available to join after being created
nextRoomId: ' '.// Room id for the next game
isNPC: false // Whether it is robot versus battle}})if(_id ! = =' ') { return _id }
throw new Error('roomId get fail')}catch (error) {
log.error(error)
throw error
}
}
// ...
Copy the code
Details: Jump to the battle page
After the room is created successfully, you can enter the battle page and invite friends to fight
// router.push('combat', {roomId}) // Jump to the battle page and pass roomIdA simple encapsulation of routing operations is doneconst pages = {
home: '/pages/home/home'.combat: '/pages/combat/combat'.wordChallenge: '/pages/wordChallenge/wordChallenge'.userWords: '/pages/userWords/userWords'.ranking: '/pages/ranking/ranking'.setting: '/pages/setting/setting'.sign: '/pages/sign/sign'
}
function to(page, data) {
if(! pages[page]) {throw new Error(`${page}is not exist! `)}const _result = []
for (const key in data) {
const value = data[key]
if ([' '.undefined.null].includes(value)) {
continue
}
if (value.constructor === Array) {
value.forEach(_value= > {
_result.push(encodeURIComponent(key) + '[] =' + encodeURIComponent(_value))
})
} else {
_result.push(encodeURIComponent(key) + '=' + encodeURIComponent(value))
}
}
const url = pages[page] + (_result.length ? `?${_result.join('&')}` : ' ')
return url
}
class Router {
push(page, param = {}, events = {}, callback = (a)= > {}) {
wx.navigateTo({
url: to(page, param),
events,
success: callback
})
}
pop(delta) {
wx.navigateBack({ delta })
}
redirectTo(page, param) {
wx.redirectTo({ url: to(page, param) })
}
reLaunch() {
wx.reLaunch({ url: pages.home })
}
toHome() {
if (getCurrentPages().length > 1) { this.pop() } else { this.reLaunch() }
}
}
export default new Router()
Copy the code
So far, the owner has entered the battle page
Watch monitors room data changes
After entering the battle room, you need to monitor the records of the current room to prepare friends, start fighting, and the complete process of fighting. This point is the core part of the whole chapter
// miniprogram/pages/combat/combat.js
onLoad(options) {
const { roomId } = options
this.init(roomId) // Initialize the room listener
},
async init(roomId) {
$.loading('Get room information... ')
/** * 1. Obtain the user openID */
const openid = $.store.get('openid')
if(! openid) {await userModel.getOwnInfo() // Perform a login to obtain the OpenID
return this.init(roomId) // Recursive call (since there is no user information, the user may call back and go directly to the battle page) - non-homeowner users go directly to the battle page
}
/** * 2. Create a listener to initialize room data */
this.messageListener = await roomModel.model.doc(roomId).watch({
onChange: handleWatch.bind(this), // Perform a callback when database data changes
onError: e= > {
log.error(e)
this.selectComponent('#errorMessage').show('Server connection is abnormal... '.2000, () => { router.reLaunch() })
}
})
},
Copy the code
HandleWatch concrete implementation, the following points to do a detailed explanation
The room information is initialized
async function initRoomInfo(data) {
$.loading('Initialize the room configuration... ')
if (data) {
const { _id, isFriend, bookDesc, bookName, state, _openid, list, isNPC } = data
if (roomStateHandle.call(this, state)) { // Check whether the current room is valid
const isHouseOwner = _openid === $.store.get('openid')
this.setData({ // Assign values to the basic room information. When these values are successfully assigned, the initialization is complete
roomInfo: {
roomId: _id,
isFriend,
bookDesc,
bookName,
state,
isHouseOwner,
isNPC,
listLength: list.length
},
wordList: list
})
// Whether it is a friend or not, the user information of the owner is initialized first, and the owner's avatar and nickname, as well as the achievements and so on.
const { data } = await userModel.getUserInfo(_openid)
const users = centerUserInfoHandle.call(this, data[0])
this.setData({ users })
// Random matching business
if(! isHouseOwner && ! isFriend) {// If the match is random and not the owner => Automatic preparation
await roomModel.userReady(_id)
}
}
$.hideLoading()
} else {
$.hideLoading()
this.selectComponent('#errorMessage').show('Battle has been disbanded.'.2000, () => { router.reLaunch() })
}
}
const watchMap = new Map()
watchMap.set('initRoomInfo', initRoomInfo)
export async function handleWatch(snapshot) {
const { type, docs } = snapshot
if (type === 'init') { // Create a listener for the first time with type init
watchMap.get('initRoomInfo').call(this, docs[0]) // Get room details
} else {
// Others: when data update or remove operations}}Copy the code
Before initializing the room, check whether the current room is valid
@param {String} state Specifies the state of the room
export function roomStateHandle(state) {
let errText = ' '
switch (state) {
case ROOM_STATE.IS_OK:
return true
case ROOM_STATE.IS_PK:
case ROOM_STATE.IS_READY:
errText = 'Room in battle, full! '
break
case ROOM_STATE.IS_FINISH:
case ROOM_STATE.IS_USER_LEAVE:
errText = 'This room battle is over.'
break
default:
errText = 'An error occurred in the room, please try again'
break
}
this.selectComponent('#errorMessage').show(errText, 2000, () => { router.reLaunch() })
return false
}
Copy the code
After successful initialization, the UI will appear as shown below:
User join, ready (common user)
Once the owner has created the room, he can invite his friends to join him
onShareAppMessage({ from{})const { data: { roomInfo: { isHouseOwner, state, roomId, bookName } } } = this
if (from= = ='button' && isHouseOwner && state === ROOM_STATE.IS_OK) { // Click invite friend to trigger share operation
return {
title: ❤ @ You, come pk[${bookName}[a, click me into '.path: `/pages/combat/combat? roomId=${roomId}`.// Ordinary users enter the small program, directly into the battle page
imageUrl: '. /.. /.. /images/share-pk-bg.png'}}},Copy the code
After a friend joins the fight, he/she also performs the room initialization steps mentioned above and listens for room information
The user clicks To join the room, triggers the prepare operation, modifies the information of common users in the database, and executes the watch callback
onUserReady: throttle(async function(e) {
$.loading('Joining... ')
const { detail: { userInfo } } = e
if (userInfo) {
await userModel.updateInfo(userInfo) // Update user information
const { properties: { roomId } } = this
const { stats: { updated = 0}} =await roomModel.userReady(roomId) // Change the room status
if(updated ! = =1) {
this.selectComponent('#errorMessage').show('Join failed, room may be full! ')
}
$.hideLoading()
} else {
$.hideLoading()
this.selectComponent('#authFailMessage').show()
}
}, 1500),
Copy the code
/ / miniprogram/model/room. Js room collection of data encapsulation, the previous article has repeatedly explain how encapsulation, can learn to the previous chapter
userReady(roomId, isNPC = false, openid = $.store.get('openid')) {
return this.model.where({
_id: roomId,
'right.openid': ' '.state: ROOM_STATE.IS_OK
}).update({
data: {
right: { openid }, // Change the openID of a common user
state: ROOM_STATE.IS_READY, // The room state changes to ready
isNPC // Whether it is man-machine versus}})}Copy the code
When the data set of the room changes, the operation in watch will be triggered to realize that the owner knows that the user has joined the room (and the user himself knows). The watch code is as follows. Both the owner and the ordinary user will trigger the Watch
const watchMap = new Map()
watchMap.set(`update.state`, handleRoomStateChange)
export async function handleWatch(snapshot) {
const { type, docs } = snapshot
if (type === 'init') { watchMap.get('initRoomInfo').call(this, docs[0])}else {
const { queueType = ' ', updatedFields = {} } = snapshot.docChanges[0]
Object.keys(updatedFields).forEach(field= > { // Iterate over the modified collection field
const key = `${queueType}.${field}` // When the user is ready, 'update.state' is executed
watchMap.has(key) && watchMap.get(key).call(this, updatedFields, snapshot.docs[0])}}}Copy the code
async function handleRoomStateChange(updatedFields, doc) {
const { state } = updatedFields
const { isNPC } = doc
console.log('log => : onRoomStateChange -> state', state)
switch (state) {
case ROOM_STATE.IS_READY: // The user is ready
const { right: { openid } } = doc
const { data } = await userModel.getUserInfo(openid)
const users = centerUserInfoHandle.call(this, data[0]) // Get the basic information, avatar, nickname, and win rate of ordinary users
this.setData({ 'roomInfo.state': state, users, 'roomInfo.isNPC': isNPC })
If the current user is the owner of the house and the mode is random match => 800ms later, the battle starts
const { data: { roomInfo: { isHouseOwner, isFriend, roomId } } } = this
if(! isFriend && isHouseOwner) { setTimeout(async() = > {await roomModel.startPK(roomId)
}, 800)}break
// case ...}}Copy the code
Start the battle (homeowner)
After the user is ready, the host’s invite friend button will be hidden, showing the start battle button, and when the click to start battle triggers the room state to be changed to PK
// Click the event
onStartPk: throttle(async function() {
$.loading('Start fighting... ')
const { properties: { roomId } } = this
const { stats: { updated = 0}} =await roomModel.startPK(roomId) // Modify the data set to trigger the watch
$.hideLoading()
if(updated ! = =1) { this.selectComponent('#errorMessage').show('Start to fail... Please try again ')}},1500)
// Operations in room.js data set (roomModel)
// ...
startPK(roomId) {
return this.model.where({
_id: roomId,
'right.openid': this._.neq(' '),
state: ROOM_STATE.IS_READY
}).update({
data: {
state: ROOM_STATE.IS_PK
}
})
}
// ...
Copy the code
Watcher’s action, as prepared by the user, will trigger the update.state callback, namely handleRoomStateChange
async function handleRoomStateChange(updatedFields, doc) {
const { state } = updatedFields
const { isNPC } = doc
console.log('log => : onRoomStateChange -> state', state)
switch (state) {
case ROOM_STATE.IS_READY: // The user is ready
// Callback service prepared by the user...
case ROOM_STATE.IS_PK: // Start the battle
this.initTipNumber() // Initialize the number of cue cards
this.setData({ 'roomInfo.state': state }) // Change the room state to change the scene
this.playBgm() // bgm
// Man-machine versus business logic, automatic selection (later chapter introduction)
isNPC && npcSelect.call(this.selectComponent('#combatComponent'))
break}}Copy the code
dynamic
The battle process is controlled by listIndex. The random word list generated by the front room is used as the battle word list. When both sides have selected the option, listIndex++ can realize the switch to the next question
// The user clicks on the options and selects the meaning of the word
onSelectOption: throttle(async function(e) {
if (!this._isSelected) {
const { currentTarget: { dataset: { index, byTip = false } } } = e
this.setData({ showAnswer: true.selectIndex: index })
const { properties: { roomId, isHouseOwner, listIndex, isNpcCombat, wordObj: { correctIndex, wordId } } } = this
let score = WRONG_CUT_SCORE
const key = isHouseOwner ? 'leftResult' : 'rightResult' // Use to display √ or × on options
// correctIndex is marked in the generated random word list. Index is the currently selected option
if (correctIndex === index) { // Select the correct one
playAudio(CORRECT_AUDIO)
this.setData({ [key]: 'true' })
score = this.getScore()
if (byTip) { // Select the option via the cue card
userModel.changeTipNumber(- 1) // Cue card-1
userWordModel.insert(wordId) // Insert new words into the table
this.triggerEvent('useTip') // The local cue card displays -1}}else { // Wrong selection
playAudio(WRONG_AUDIO)
wx.vibrateShort()
this.setData({ [key]: 'false' })
userWordModel.insert(wordId) // Insert words into the vocabulary table
}
const { stats: { updated = 0}} =await roomModel.selectOption(roomId, index, score, listIndex, isHouseOwner) // Modify the selection of the current user of the data set
if (updated === 1) { this._isSelected = true } else {
this.setData({ showAnswer: false.selectIndex: - 1 }) //
}
// Man-machine versus business
isNpcCombat && this.npcSelect()
} else {
wx.showToast({
title: 'This is selected. Don't click too fast.'.icon: 'none'.duration: 2000})}},1000),
Copy the code
// room collection instance
// roomModel.selectOption
selectOption(roomId, index, score, listIndex, isHouseOwner) {
const position = isHouseOwner ? 'left' : 'right'
return this.model.doc(roomId).update({ // Trigger watch after update
data: {
[position]: {
gradeSum: this._.inc(score),
grades: {
[listIndex]: {
index,
score
}
}
}
}
})
}
Copy the code
Execute watcher when the value of left.gradeSum or right.gradeSum changes
In the watcher callback function, if the question is chosen by oneself, the result of the other party is displayed if both parties have chosen (cannot be displayed before their choice). When both parties have chosen the last question, the settlement process enters
import $ from '. /.. /.. /utils/Tool'
import { userModel, roomModel } from '. /.. /.. /model/index'
import { roomStateHandle, centerUserInfoHandle } from './utils'
import { ROOM_STATE } from '.. /.. /model/room'
import { sleep } from '.. /.. /utils/util'
import router from '. /.. /.. /utils/router'
const LEFT_SELECT = 'left.gradeSum'
const RIGHT_SELECT = 'right.gradeSum'
async function handleOptionSelection(updatedFields, doc) {
const { left, right, isNPC } = doc
this.setData({
left,
right
}, async() = > {this.selectComponent('#combatComponent') && this.selectComponent('#combatComponent').getProcessGrade()
const re = /^(left|right)\.grades\.(\d+)\.index$/ // left.grades.1.index
let updateIndex = - 1
for (const key of Object.keys(updatedFields)) {
if (re.test(key)) {
updateIndex = key.match(re)[2] // Index of the selected item (the answer to the selected item)
break}}if(updateIndex ! = =- 1 && typeofleft.grades[updateIndex] ! = ='undefined' &&
typeofright.grades[updateIndex] ! = ='undefined') { // Both sides have chosen this topic, need to switch to the next question
this.selectComponent('#combatComponent').changeBtnFace(updateIndex) // Displays the result of the other party's selection
const { data: { listIndex: index, roomInfo: { listLength } } } = this
await sleep(1200)
if(listLength ! == index +1) { // The question is not over, switch to the next question
this.selectComponent('#combatComponent').init()
this.setData({ listIndex: index + 1= > {}, ()this.selectComponent('#combatComponent').playWordPronunciation()
})
isNPC && npcSelect.call(this.selectComponent('#combatComponent'))}else {
this.setData({ 'roomInfo.state': ROOM_STATE.IS_FINISH }, () => {
this.bgm && this.bgm.pause()
this.selectComponent('#combatFinish').calcResultData()
this.showAD()
})
}
}
})
}
const watchMap = new Map()
watchMap.set(`update.${LEFT_SELECT}`, handleOptionSelection)
watchMap.set(`update.${RIGHT_SELECT}`, handleOptionSelection)
export async function handleWatch(snapshot) {
const { type, docs } = snapshot
Object.keys(updatedFields).forEach(field= > {
const key = `${queueType}.${field}`
watchMap.has(key) && watchMap.get(key).call(this, updatedFields, snapshot.docs[0])})}Copy the code
At this point, the realization of the battle process, the end of the battle settlement, we share in the following article
This book is a practical series to share, will continue to update, revision, thank the students to study together ~
The open source project
Since this project participated in the university competition, we will not open source for the time being. After the competition, we will open source (code + design drawing) on wechat official account: Join-fe. Please don’t miss it
With the series of articles, you can go to the old bag students digging gold homepage edible