In 2019, when most basketball mini-games on the market were just "pure shooting" gameplay, a casual competitive mini-game "Basketball Duel" featuring PVP real-time 1v1 made players excited, and was launched on WeChat, QQ, Toutiao, etc. The game reached 30 million people within one year after its release.
The excellent performance of the mini game version has strengthened the R&D team's confidence in launching an app version. After nearly a year of polishing, the mobile game version of "Basketball Duel" has become increasingly mature and accumulated players, recently being ranked first in the AppStore sports game list.
Whether the mini game version or the native version, "Basketball Duel" is developed using Cocos Creator. But, there are differences between the native version and the mini game version. What changes or optimizations have been made? This time, "Boss Crab" from SAGI GAMES, the main program of "Basketball Duel," will share with us the "evolution" experience from web game to APP.
It is not just as simple as porting from a mini game to an app. On the mini-game platform, "sports " is a niche category with few excellent products. The centralized platform provides recommended diversion, and good products can quickly stand out. On the native platform, however, the team has to fight a bloody path in the market to compete in the fierce competition.
After developing the mobile version, the project team gradually expanded from the original four people to dozens of people to make the game more playable and have better visual effects. On the technical level, we have comprehensively optimized the game from the aspects of PVP battle implementation and optimization, AI behavior tree design, difficulty configuration, Spine system improvements, advanced skills, etc. This allowed for higher quality performance on native platforms.
PVP battle
At first, we thought it was a pseudo-proposition to do real-life matchmaking on mini-games, which was not seen as a good idea. Still, when the game grew to 42,000 people, the call for real-life matchmaking grew, making us re-examine the proposition of PVP.
- In traditional io games, due to a large number of participants in one round, it is challenging to complete the round, and it is not worthwhile to do PVP.
- The main focus of "Basketball Duel" is 1v1 battles. Players can bet in-game cards to start a match. The replay rate is high, and the characters have numerical ways to grow. There are elements of nurturing, and PVP is entirely feasible.
We quickly established a frame synchronization scheme since "Basketball Duel" was developed using the ECS structure and naturally supports frame synchronization. For details, please refer to this popular science article by a community leader: "Tencent Senior Engineer Bao Ye: Frame synchronization games are in Implementation details at the technical level.
On the mini game platform, we used Tencent Cloud Online Battle Platform (MGOBE). " Basketball Duel" is an early partner of MGOBE. As early as the end of 2019, before MGOBE was officially released, "Basketball Duel" was already cooperating with Tencent Cloud for testing. As a middle-aged and elderly programmer who has been developing small games full-time for ten years, I think MGOBE is indeed the best choice on the small game platform. Unfortunately, MGOBE will stop its service, and we will find another battle platform.
After starting the APP development, we used Go to write a set of online battle platforms regarding MGOBE. The API names are the same, and the client can be adjusted without significant changes. When writing the platform, we focused on the following areas:
- Use distributed and automatic, flexible deployment: When the peak suddenly increases sharply, new servers will be automatically deployed to adapt to the changes, and after the peak is reduced, the server is automatically reduced.
- Protocol compression: The operating instructions are expressed in one byte, and other protocols are also simplified as much as possible.
- Support TCP and UDP: Because UDP has redundant instructions when the network is not good, the message packets will be backlogged, which is not very friendly to low-end mobile phones. Therefore, TCP is still used for low-end mobile phones to connect to battles.
- Detailed logging and monitoring tools.
Out of sync issue
Since frame synchronization is entirely dependent on client computing, it is required that the calculation results of the two clients are precisely the same during the game; otherwise, the picture will be out of sync.
In the mini-game version of "Basketball Duel," because I was also doing frame synchronization for the first time and had no experience, there were a lot of out-of-sync situations in the first version. After investigation, the reasons were roughly as follows:
- The problem with floating-point numbers: 0.3+0.4=0.7, but 0.7-0.4!==0.3. Although the operation of rounding or retaining two decimal places is done, there are always omissions.
- The problem of inconsistency in input parameters: When you realize that both characters played the game from the left perspective when I initialized the battle, we not only flipped the character data, but we also flipped the battle command. As a result, the input parameters at both ends of the calculation were inconsistent. When it needs to be negated, it will cause a large number of floating-point problems.
- The problem of battlefield reset: after the end of the previous battle, some data was not cleared and brought into the next battle, resulting in out-of-sync issues.
- Inconsistent order of operations: who touches the ball first?
The solution is as follows:
- Logically, the host is always on the left, and the visitor is on the right, but the visitor flips the UI to meet the visual requirement of being on the left.
- We optimized data cleaning logic before and after the score.
- Check for logical bugs.
There is also a critical question here: how do you find out if the player is out of sync during the online gameplay?
Since frame synchronization relies on client-side calculus, the server cannot know whether the player is out of sync. Therefore, out-of-sync troubleshooting can only be analyzed by client logs. My approach is:
- Every second, both parties combine the critical battlefield data of the local end, encrypt it as md5, and send it to the other party for verification. If it is not equal, each party will report its own log to the log server;
- After the log server receives the two log files, it will generate a text comparison page (similar to beyond compare) and send the link to the company's IM robot to compare the log entries to determine where the out-of-sync problem is.
Reconnection and frame tracking
There are two cases of reconnection:
- The game is disconnected and reconnected.
- Reconnect after the game exits abnormally (need to restore the battlefield).
When the client receives the frame message, it will save the current frame number. After disconnection, since the server will not stop sending frame broadcasts, the frame will be interrupted after reconnection. At this time, the game logic cannot continue, and the SDK must send a request to supplement the frame interface, retrieve the missing frame message, insert it into the frame queue, and forward it to the logic layer to drive the game. When it is necessary to perform frame chasing operation, accelerate the calculation at the logic layer, process the frame queue cleanly, and the game will naturally catch up. When chasing frames, it can be processed all within one dt, or it can process several seconds of data per dt, so that the CPU can take a breath, and the effect of fast-forwarding on the screen can be achieved.
Chasing frames after restarting the game is basically the same as chasing frames in the middle of the game. The difference is that you need to restore the battlefield and start chasing from the first frame. It is not too much of a considerable pressure to chase frames from the beginning, as most duels last 1-2 minutes for a single game of "Basketball Duel."
Other PVP experience
- FPS frame number: Since the network message processing is also queued in the main loop of the engine, if the performance of the game itself is not optimized well enough and the FPS is too low, it will affect the quality of the battle. It is recommended that the FPS be at least >55.
- AI intervention: In the game, players often encounter a situation where one side abandons the game due to losing by a significant score, so AI intervention is required to ensure player experience. Because the frame synchronization server has no logic and only forwards, the client itself currently implements the AI intervention. When our end detects that the other party is offline, our end will assign AI behavior to the opponent, and all operations of AI will send action commands to the server in the "identity of the other party," which can ensure that the other party can follow the frame normally after reconnecting.
AI Behavior Tree
The APP version is consistent with the AI development of the mini-game and is still BehaviorTree3.
Design ideas
The character has three states on the court: Offense/Defense/Neither has the ball. The behavior tree design revolves around these three states:
- When attacking, what do you do?
- Judging what can be done? Do a skill/layup/dunk/break/shoot (the character uses a finite state machine).
- Considering the distance from the basketball (different distances have different shot probabilities).
- What stage do you jump to?
- When defending, what do you do?
- Approach your opponent.
- Whether to grab the ball.
- The opponent has taken a shot. Should I be running or blocking?
- What do you do without the ball?
- Is the ball in the air? Is there a scrimmage?
- Is the ball on the ground? Pick it up now!
In general, the design of the behavior tree is similar to the flow chart, and all the branches are designed well.
Difficulty control
Taking the classic 11-point gameplay as an example, I created ten behavior trees for ten difficulty levels in the mini-game. The difference between each behavior tree is minimal. The main difference lies in the AI's processing delay and entry probability.
On the APP, planning requires finer control over time. If there is a difficulty level of 100, 100 behavior trees must be created according to the original method... This is obviously unrealistic. The solution is to set entry points at the entrance of each specific behavior of AI and combine the configuration table to control the probability and delay of the behavior.
Spine Dressing System
Costume ideas for multiple roles and multiple suits
On the mini game platform, we refer to the replacement implementation in the Cocos Creator example, replacing the attachment on the slot on another spine with the attachment on another spine.
Our usage is slightly different. All characters, initial skins, and suit skins are in the same spine file. When using it, I first read the getRuntimeData() of the current Spine to obtain the real-time data of the corresponding suit skin and then take the costume. The attachment in the slot replaces the attachment in the same slot of the current character skin. It's too lengthy. Let's paste the code:
changeCloth (skinName: string, slotName: string): any {
let spine: sp.Skeleton = this.node.spine
let skeletonData = spine.skeletonData.getRuntimeData()
let skin = skeletonData.findSkin(skinName)
const slot = spine.findSlot(slotName)
const slotIndex = skeletonData.findSlotIndex(slotName)
const attachment = skin.getAttachment(slotIndex, slotName)
slot.setAttachment(attachment)
}
But with more and more characters, the Spine becomes more and more challenging to maintain. Fifty characters + 20 costumes are in a spine. It takes nearly 10 minutes to export, and the output textures are also massive and require paging.
I thought of splitting 50 characters into 50 spines, which brought another problem: there are animations in the spines, 50 spines need to copy the animations 50 times, and each new animation has to be synchronized to all spines.
After racking my brain, I came up with a way to combine the new skins:
- Split the Spine into three: base.spine /skin.spine /suit.spine, the functions are:
- The base.spine contains animation definitions and the limbs of the base three-color skin (dark skin/white skin/yellow skin), but without face maps;
- Skin. The Spine contains the character's face and base skin;
- Costume. The spine is the suit definition.
- The new sp. Spine.Skin comes with skin. First, copy the skin color in the base.spine, then take out the character skin from skin. Spine and add it to newSkin, and finally take out the attachment of the suit from the suit.spine and overlay it again In the newSkin. A new skin is finally formed, added to the characters' skins on the field, and then setSkin(newSkin) to generate a new appearance.
- Note a premise that the slot structures of the three spines should be precisely the same and in the same order so that there will be no bugs when overwriting.
Paste a code snippet to demonstrate:
export default class SpineUtil {
static async setSkin (spine: sp.Skeleton, heroId, skinId, collocation = {}) { // posType 1 headwear 2 tops 3 pants 4 shoes 5 hands 6 legs
const skeletonData = spine.skeletonData.getRuntimeData()
const baseClothesData = await AssetLoader.loadResAsync(`spine/clothes/c_${heroId}/c_${heroId}`, sp.SkeletonData)
const baseClothesDataRuntimeData = baseClothesData.getRuntimeData()
const baseClothesSkin = baseClothesDataRuntimeData.findSkin('c_' + skinId)
if (!baseClothesSkin) return
let newSkinName = 'newSkin' + heroId + skinId
for (let pos in collocation) {
newSkinName += '_' + collocation[pos]
}
const newSkin = new sp.spine.Skin(newSkinName)
const { SkinColor } = app.db.actor.GetActorById(heroId)
const findSkin = skeletonData.findSkin(['white', 'yellow', 'black'][SkinColor - 1])
newSkin.copySkin(findSkin)
// 使用默认外观
for (const skinEntry of baseClothesSkin.getAttachments()) {
const slot = !cc.sys.isNative ? skinEntry.slotIndex : baseClothesSkin.getEntrySlot(skinEntry)
const name = !cc.sys.isNative ? skinEntry.name : baseClothesSkin.getEntryName(skinEntry)
const attachment = !cc.sys.isNative ? skinEntry.attachment : skinEntry
this.addAttachment(SKIN_PART, newSkin, slot, name, attachment)
this.addAttachment(ARM_PART, newSkin, slot, name, attachment)
}
... 省略部分代码
if (skeletonData.skins[skeletonData.skins.length - 1].name === newSkin.name) {
skeletonData.skins[skeletonData.skins.length - 1] = newSkin
} else {
!cc.sys.isNative ? skeletonData.skins.push(newSkin) : skeletonData.addSkin(newSkin)
}
spine.setSkin(newSkinName)
}
}
You may have noticed that the native API here is different from the js one because some methods and attributes in the mobile version are not exported, such as new sp. Spine.Skin will report an error on mobile devices, so we modified it again under Spine c++ spine runtime.
Some other tricks for Spine
- Use the empty bone to realize the release point of the character shooting. The game takes out the world coordinates of the shot point as the starting point for the ball to fly out, and the animation can control it. There is no need to develop and write an unintuitive coordinate, as shown in the figure:
- Use multitrack playback to achieve local special effects on the character's skin.
- Use zoom to express the height difference of the character or adapt to the display needs of different interfaces.
Cocos Creator 3.6 will fully optimize Spine performance and give it a facelift, and this version is expected to be released in the middle of this year.