Implementing Gacha/Lootbox Mechanics in a Mobile Game
Imagine: your RPG is almost ready, only the gacha remains to be integrated, but user testing shows that players quit after 50 rolls without a legendary — a combination without a pity system kills retention. Or worse: you release an update, and the next day the App Store rejects it — probabilities are not displayed. Such problems cost money: rework and re-moderation can take weeks, while competitors steal your audience. We help avoid this: in 3–4 days we set up a basic gacha with fail-safes, in 7–10 days a full system with banners, animations, and reports for the marketplace. Our experience: over 40 projects, each passed review on the first try. Don't risk your release — contact us for an audit of your current mechanics.
Mathematical Probability Model
Basic gacha is a weighted random selection from a reward pool. Simple implementation:
data class GachaItem(val id: String, val rarity: Rarity, val weight: Int) fun rollGacha(pool: List<GachaItem>): GachaItem { val totalWeight = pool.sumOf { it.weight } var random = Random.nextInt(totalWeight) for (item in pool) { random -= item.weight if (random < 0) return item } return pool.last() } Example weight distribution:
- Common: weight 1000 → ~58.8% chance
- Rare: weight 500 → ~29.4%
- Epic: weight 150 → ~8.8%
- Legendary: weight 50 → ~2.9%
Total weight: 1700. Legendary chance: 50/1700 ≈ 2.94%.
How the Pity System Works
Pity (guaranteed reward) is a mechanic that guarantees a legendary after N unsuccessful attempts. Without pity, a player could have a streak of 200 rolls without a legendary — mathematically possible, practically destructive to the experience.
Soft pity — starting from the N-th roll, the legendary probability increases. Genshin Impact uses this approach: from the 74th roll, the 5* chance increases by 6% per roll.
Hard pity — exactly on the N-th roll, a legendary is guaranteed. Easier to implement and communicate honestly to the player.
data class PlayerGachaPity( val rollsSinceLastLegendary: Int, val softPityStart: Int = 74, val hardPityAt: Int = 90 ) fun calculateEffectiveProbability(baseProbability: Float, pity: PlayerGachaPity): Float { if (pity.rollsSinceLastLegendary >= pity.hardPityAt) return 1.0f if (pity.rollsSinceLastLegendary >= pity.softPityStart) { val excess = pity.rollsSinceLastLegendary - pity.softPityStart return minOf(1.0f, baseProbability + excess * 0.06f) } return baseProbability } The pity counter is stored on the server, not on the client. A client-side counter would reset on reinstallation — the player loses accumulated pity, causing justified outrage and chargebacks.
Why Server-Side Generation?
The result of a gacha roll must not be generated on the client. Server-side generation is 100 times more secure than client-side — almost impossible to hack. Flow:
- Client sends a request
POST /gacha/rollwithuserId,gachaPoolId, number of rolls, payment token - Server checks balance/transaction, retrieves pity counter, generates result using seeded PRNG
- Server records the result, updates pity, returns
rollId+ results - Client plays animation and displays results
The PRNG seed is generated on the server; the client cannot predict or influence the result.
Unboxing Animation
Gacha roll animation is not just decoration; it's a psychologically significant moment. Duration 2–4 seconds, building suspense before the reveal. For mobile implementation we use:
- Unity Animator or Spine for 2D card/egg animations
- Particle System for effects on rare reward reveals
- Haptic feedback (UIImpactFeedbackGenerator on iOS, VibrationEffect on Android) at the moment of reveal
Multi-pull animations (x10 roll) require separate logic: show all results sequentially with increasing tempo, common ones fast, legendary with full-screen effects.
Regulatory Requirements
The App Store requires displaying probabilities "before purchase." Google Play has a similar requirement. Implementation: a "Gacha Pool Details" screen with a probability table by reward type. This screen is accessed via an "i" (info) button next to the roll button.
| Rarity | Probability | Guarantee (hard pity) |
|---|---|---|
| Common | 58.8% | — |
| Rare | 29.4% | — |
| Epic | 8.8% | 40 rolls |
| Legendary | 2.9% | 90 rolls |
What Types of Gacha Banners Exist?
| Banner type | Duration | Rate-up | Pity compatibility | Revenue |
|---|---|---|---|---|
| Standard | Permanent | No | Yes | Stable |
| Rate-up | 7–14 days | Yes | Yes (with guarantee) | High |
| Featured | 7–14 days | Yes | 50/50 guarantee | Maximum |
Rate-up banner — temporary pool with increased chance for a specific character/item. Creates urgency and revenue peaks. Standard banner — permanent pool, receives "lost" pity wins in guarantee systems. Featured banner with guarantee — if rate-up does not drop on the 1st legendary, the next guaranteed will be featured ("50/50" system).
Checklist: What's Included in the Work
- Requirements analysis and mathematical model design
- Server-side and client-side logic implementation (Swift/Kotlin/Flutter)
- Pity system and rate-up banner integration
- UI layout with probability display
- Unboxing animation creation (2D/3D)
- Load testing (500+ simultaneous rolls)
- Document preparation for App Store and Google Play submissions
- Post-release support: monitoring, metric-driven improvements
Estimated Timeframes
Basic gacha with pity and server-side generation — 3–4 days. Full system with multiple banner types, probability UI, animations, and analytics — 7–10 days. The cost is calculated individually — reach out to us for an estimate.
Get a consultation — we'll help design a gacha system that generates profit and passes moderation.







