CCFish uses a shared TypeScript core with Cocos Creator 2.4.15 that compiles to all platforms from a single codebase — enabling simultaneous feature releases across iOS, Android, and Web while cutting development time by 40%.

The Problem

Platform fragmentation is a silent killer in mobile game development. Before CCFish adopted its current architecture, the team maintained three separate codebases — one each for iOS, Android, and Web. Every feature had to be built three times. Every bug had to be fixed three times. Release coordination became a nightmare: shipping a new fish species or tournament mode meant staggered rollouts that could span weeks.

The costs were staggering. A feature that should take one developer two weeks instead consumed three developers working in parallel across platforms — and that assumes none of them got blocked waiting for the others. In reality, the iOS build might uncover an issue that was already fixed on Android, or the Web version would use a different ad SDK that behaved differently under load. Each difference meant more QA cycles, more bug tickets, and more context-switching overhead.

Beyond the direct engineering cost, platform fragmentation hurt the business. When a new limited-time event couldn't ship simultaneously on all platforms, player engagement dropped. Players on the lagging platform would lose interest before their version arrived. Competitive features like the global fishing leaderboard became inconsistent. The team knew something had to change.

The Solution

Cocos Creator 2.4.15 provides a cross-platform game engine that compiles to iOS (via Xcode), Android (via Gradle), and Web (via HTML5). By itself, the engine solves the rendering layer — but the team needed more. They needed a shared TypeScript core that held all game logic, networking, and storage code, independent of any platform-specific SDK or rendering concern.

The solution was a three-layer architecture:

| Layer | Responsibility | Platform Dependency |

|-------|---------------|-------------------|

| Shared Core | Game logic, ECS components, networking, storage, event bus | None — pure TypeScript |

| Platform Adapters | IAP, push notifications, social sharing, ads SDK | Platform-specific per adapter |

| Cocos Creator Render | Scene management, sprite rendering, audio, input | Cocos engine (compiles per target) |

The Shared Core layer contains approximately 65% of the total codebase. It has zero imports from Cocos Creator or any native SDK. This means it can be unit-tested in Node.js without a browser or device — a massive win for developer velocity.

Architecture Overview

Layer 1: Shared Core

The Shared Core is organized around a lightweight Entity-Component-System (ECS) pattern. Rather than using a full ECS framework like Unity's, CCFish built a minimal implementation that gave them the benefits of composition without the overhead.

```typescript

// Simplified ECS core

interface Component {

type: string;

data: Record<string, unknown>;

}

class Entity {

id: number;

private components: Map<string, Component> = new Map();

addComponent(component: Component): void {

this.components.set(component.type, component);

}

getComponent<T>(type: string): T | undefined {

return this.components.get(type)?.data as T;

}

hasComponent(type: string): boolean {

return this.components.has(type);

}

}

class System {

update(entities: Entity[], dt: number): void {

// Override in subclass

}

}

```

Game entities like fish, hooks, power-ups, and UI panels are composed of components rather than arranged in deep inheritance hierarchies. This makes the system trivially extensible — adding a new power-up means adding a few new components and a system to process them, not touching any existing code.

An event bus decouples systems from each other. When a fish is caught, the FishingSystem emits a `FISH_CAUGHT` event. The InventorySystem listens for it and updates the player's collection. The AchievementSystem listens for it and checks milestone thresholds. The UISystem listens for it and plays an animation. No system knows about the others — they only know about events.

```typescript

// Event bus pattern

type EventHandler = (payload: any) => void;

class EventBus {

private handlers: Map<string, EventHandler[]> = new Map();

on(event: string, handler: EventHandler): void {

if (!this.handlers.has(event)) {

this.handlers.set(event, []);

}

this.handlers.get(event)!.push(handler);

}

emit(event: string, payload: any): void {

this.handlers.get(event)?.forEach(h => h(payload));

}

off(event: string, handler: EventHandler): void {

const hs = this.handlers.get(event);

if (hs) {

this.handlers.set(event, hs.filter(h => h !== handler));

}

}

}

```

Networking is handled by a custom WebSocket manager with automatic reconnection, message queuing, and serialization. Storage uses a key-value abstraction that maps to localStorage on Web, UserDefaults on iOS, and SharedPreferences on Android — all through the platform adapter layer.

Layer 2: Platform Adapters

The Platform Adapter layer uses the Provider Interface pattern. Each platform service (IAP, push notifications, social sharing, ads) defines an interface in the Shared Core. Platform-specific implementations are injected at build time.

```typescript

// IAP Provider Interface

export interface IAPProvider {

initialize(): Promise<void>;

purchase(productId: string): Promise<PurchaseResult>;

restorePurchases(): Promise<PurchaseResult[]>;

getProducts(): Promise<Product[]>;

}

// iOS Implementation

class iOSIAPProvider implements IAPProvider {

async purchase(productId: string): Promise<PurchaseResult> {

// Call StoreKit via Objective-C bridge

}

}

// Android Implementation

class AndroidIAPProvider implements IAPProvider {

async purchase(productId: string): Promise<PurchaseResult> {

// Call Google Play Billing

}

}

```

At build time, a dependency injection container selects the correct implementation based on the build target. The Shared Core never needs to know which platform it's running on — it just calls `this.iap.purchase('golden_rod')` and trusts the adapter to handle it.

Layer 3: Cocos Creator Rendering

The rendering layer is pure Cocos Creator — scenes composed of nodes, sprites, animations, and UI widgets. What makes this architecture different is that the rendering layer is thin. It binds to the Shared Core through an interface called `RenderBridge` that exposes methods like `spawnFish(entity)`, `updatePosition(entityId, x, y)`, and `playAnimation(name)`.

This separation means the Cocos Creator layer can be swapped or upgraded without touching game logic. When the team eventually migrates to Cocos Creator 3.x, the Shared Core won't change at all — only the RenderBridge implementation will need updating.

Implementation Patterns

Three key patterns made this architecture work in practice:

**1. Component-based entity composition.** Instead of class hierarchies like `BossFish extends AngryFish extends Fish extends GameObject`, entities are flat containers of components. A `BossFish` entity has a `BossAIComponent`, a `SpriteComponent`, a `HealthComponent`, and so on. This avoids diamond inheritance problems and makes it trivial to add new behaviors.

**2. Event-driven system decoupling.** The event bus is the backbone of cross-system communication. It's also serializable — events can be logged, replayed, and even forwarded over the network for multiplayer scenarios. The team built a visual event inspector that shows all events flowing through the system in real time, which became invaluable for debugging complex interactions.

**3. Provider interface for platform services.** Every platform-specific capability is hidden behind an interface. The Shared Core defines what it needs; adapters provide it. New platforms (like a desktop Steam build) can be added by writing new adapters without touching the core.

Results

The numbers speak for themselves. After migrating to the shared TypeScript core architecture:

- **40% faster feature delivery.** Features that took 6 developer-weeks now take 3.5.

- **Single codebase covering iOS, Android, and Web.** One branch, one CI pipeline, one set of unit tests.

- **Reduced QA surface by 60%.** Bugs that were previously iOS-only or Android-only are now caught in the shared core before they reach platform-specific code.

- **Simultaneous releases across all platforms.** Every feature ships on all three platforms on the same day.

- **80% unit test coverage on the Shared Core.** Tests run in Node.js in under 30 seconds.

Key Takeaways

Building a cross-platform game with a shared TypeScript core isn't just possible — it's the right approach for any team shipping on multiple platforms. The key architectural decision is enforcing strict layer boundaries. The Shared Core must never import platform-specific code. The Render Layer must never contain game logic. When those boundaries are respected, the result is a codebase that's faster to develop, easier to test, and more adaptable to future platforms.

For teams considering this approach, start with the Provider Interface pattern. Identify every platform-specific service in your game and define an interface for it. Everything else — the ECS, the event bus, the networking, the storage — lives in pure TypeScript and runs anywhere. That 40% reduction in dev time is achievable, but only if you commit to the separation from day one.