Firebase Backend v1.5

Setup & Run Guide

Scope ‑ This document explains how to integrate the BulletHell Elemental Template v1.4 with Firebase Authentication and Cloud Firestore. It covers both a Quick Start for experienced users and a Step‑by‑Step walkthrough with screenshots for newcomers.


Read the offline backend page first


1. Quick Start (Experienced Users)

Unity

  • Unity Version: 2022.3 LTS (or newer)

Unity Packages (via Package Manager)

  • Input System

  • AI Navigation

  • In-App Purchasing

  • 2D Sprite

  • TextMeshPro (TMP)

Firebase SDK

Import the following Firebase SDK packages (same major/minor, recommended):

  • FirebaseAuth 12.10.1

  • FirebaseFirestore 12.10.1

  • FirebaseDatabase (Realtime Database) 12.10.1

Project setup:

  • In Project Settings → Player → Scripting Define Symbols, add:

    • FIREBASE

  • Import the BulletHell Elemental Template.

  • Add scenes to Build Settings in this order:

    1. Login

    2. Home

    3. Other gameplay/maps scenes

  • Tags / Layers:

    • Ensure the Monster tag exists and is assigned to all enemy prefabs.

  • google-services.json:

    • Download from Firebase Console.

    • Place in: Assets/StreamingAssets/ (create the folder if missing).

    • File name must remain google-services.json.


2. Firebase Console Setup

2.1 Authentication

In Authentication → Sign-in method:

  • Enable:

    • Email/Password

    • Anonymous

No special action needed here for user IDs; they are handled by the game code using Firebase Auth UID + Firestore metadata.


2.2 Firestore Database

Create a Cloud Firestore database in Production mode (or test mode during development, but with the rules below).

Required Collections & Documents

  1. Players

    • Will be created automatically by the game on first login/registration.

    • No manual documents required.

  2. BattlePass / SeasonInfo

    Create:

    • Collection: BattlePass

    • Document ID: SeasonInfo

    • Fields:

      • SeasonNumber, e.g. 1

      • StartSeasonTimestamp, season start date

      • (Optional) DurationDaysNumber, total days for the season

  3. Admin / Admin

    Create:

    • Collection: Admin

    • Document ID: Admin

    • Fields:

      • gmArray of string

        • Each entry is a Firebase Auth UID of a GM user.

      • supportArray of string

        • Each entry is a Firebase Auth UID of a support user.

    These arrays control who can use GM tools (Events, Mailbox, Global Message, Ban, etc).

  4. Admin / Events

    Create:

    • Collection: Admin

    • Document ID: Events

    • You can leave it empty or add fields according to your tooling.

    • Used by the in-game GM tools for map events (must exist for writes under provided rules).

  5. Admin / GlobalMessage

    Create:

    • Collection: Admin

    • Document ID: GlobalMessage

    • Used for global announcements.

  6. Admin / Mailbox

    Create:

    • Collection: Admin

    • Document ID: Mailbox

    • Can start empty.

    • Used by GM tools to send reward mails.

  7. Friendships

    • Create a root-level collection: Friendships.

    • Add one placeholder document (for example placeholder), so the collection exists.

      • Any shape is fine when created via Console (rules don’t block console writes).

    • Actual friendship documents are created and managed by the game code using the rules below.

  8. Meta / UserCounters

    Used to assign incremental numeric userId values.

    • Create collection: Meta

    • Create document: UserCounters

    • Add field:

      • lastUserIdNumber

        • Example: 1000 (or 0, or any starting point you prefer)

    • The game will atomically increment this value when new users are provisioned.

2.3 Firestore Security Rules

Paste the following into Firestore → Rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    /* Helpers */

    function isSignedIn() {
      return request.auth != null;
    }

    function isOwner(uid) {
      return isSignedIn() && request.auth.uid == uid;
    }

    function hasRole(list, uid) {
      return list is list && uid in list;
    }

    /**
     * Returns true if the current user UID is listed as GM or Support
     * inside Admin/Admin document ("gm" or "support" string arrays).
     */
    function isGmOrSupport() {
      return isSignedIn()
        && (
          hasRole(
            get(/databases/$(database)/documents/Admin/Admin).data.gm,
            request.auth.uid
          )
          || hasRole(
            get(/databases/$(database)/documents/Admin/Admin).data.support,
            request.auth.uid
          )
        );
    }
    
    /* PLAYERS */

    match /Players/{uid} {
      allow get, list: if isSignedIn();

      allow create: if isOwner(uid);
      allow delete: if isOwner(uid);

      // Owner can update anything.
      // GM/Support can only change "isBanned".
      allow update: if
        isOwner(uid)
        || (
          isGmOrSupport()
          && resource.data.diff(request.resource.data).changedKeys().hasOnly(['isBanned'])
        );

      match /PurchasedItems/Items/List/{itemId} {
        allow get, list, create, update, delete: if isOwner(uid);
      }

      match /{document=**} {
        allow get, list, create, update, delete: if isOwner(uid);
      }
    }

    /* PLAYER NAMES */

    match /PlayerNames/{nameLower} {
      allow get: if true;
      allow list: if false;

      allow create: if isSignedIn()
        && request.resource.data.keys().hasOnly(['ownerUid', 'createdAt'])
        && request.resource.data.ownerUid == request.auth.uid;

      allow update, delete: if isSignedIn()
        && resource.data.ownerUid == request.auth.uid
        && request.resource.data == resource.data;
    }

    /* CURRENCIES */

    match /Players/{uid}/Currencies/{coinId} {
      allow get: if isOwner(uid);
      allow list: if false;
      allow delete: if false;

      allow create: if isOwner(uid)
        && request.resource.data.keys().hasOnly(['initialAmount', 'amount'])
        && request.resource.data.initialAmount is int
        && request.resource.data.amount is int
        && request.resource.data.initialAmount >= 0
        && request.resource.data.amount >= 0;

      allow update: if isOwner(uid)
        && request.resource.data.keys().hasOnly(['amount'])
        && request.resource.data.amount is int
        && request.resource.data.amount >= 0;
    }

    /* BATTLE PASS META */

    match /BattlePass/{docId} {
      allow get: if isSignedIn() && docId == "SeasonInfo";
      allow list: if false;
      allow create, update, delete: if false;
    }

    /* ADMIN / ROLES */

    match /Admin/Admin {
      allow get: if isSignedIn();
      allow list: if false;
      allow create, update, delete: if false;
    }

    /* ADMIN MAP EVENTS */

    match /Admin/Events {
      allow get: if isSignedIn();
      allow list: if false;
      allow create, update, delete: if isGmOrSupport();
    }

    /* ADMIN MAILBOX */

    match /Admin/Mailbox {
      allow get, list, create, update, delete: if isGmOrSupport();
    }

    /* ADMIN GLOBAL MESSAGE */

    match /Admin/GlobalMessage {
      allow get: if isSignedIn();
      allow list: if false;
      allow create, update, delete: if isGmOrSupport();
    }

    /* OTHER ADMIN DOCS DEFAULT */

    match /Admin/{other} {
      allow get: if isSignedIn();
      allow list, create, update, delete: if false;
    }

    /* FRIENDSHIPS */

    match /Friendships/{fid} {

      function validUsersArray(data) {
        return data.users is list
          && data.users.size() == 2
          && data.users[0] is string
          && data.users[1] is string
          && data.users[0] != data.users[1];
      }

      function sortedIdFor(data) {
        let a = data.users[0];
        let b = data.users[1];
        return (a < b) ? (a + "_" + b) : (b + "_" + a);
      }

      function isParticipant(uid) {
        return uid in resource.data.users;
      }

      function userIndex(uid) {
        return resource.data.users[0] == uid ? 0 :
               (resource.data.users[1] == uid ? 1 : -1);
      }

      function isRequester(uid) {
        return resource.data.requestedBy == uid;
      }

     function friendshipTransitionAllowed() {
        let before = resource.data.state;
        let after = request.resource.data.state;
        let uid = request.auth.uid;
        let idx = userIndex(uid);

        return (
          (before == "PENDING" && after == "ACCEPTED" && !isRequester(uid)) ||
          (before == "PENDING" && after == "PENDING" && isRequester(uid)) ||
          (after == "BLOCKED_0" && idx == 0) ||
          (after == "BLOCKED_1" && idx == 1)
        );
      }

      // Read: only participants.
      allow get, list: if isSignedIn() && isParticipant(request.auth.uid);

      // Create: send friend request (PENDING).
      allow create: if isSignedIn()
        && validUsersArray(request.resource.data)
        && sortedIdFor(request.resource.data) == fid
        && request.auth.uid in request.resource.data.users
        && request.resource.data.requestedBy == request.auth.uid
        && request.resource.data.state == "PENDING"
        && request.resource.data.createdAt is timestamp
        && request.resource.data.updatedAt is timestamp;

      // Update: accept, block, or refresh pending.
      allow update: if isSignedIn()
        && isParticipant(request.auth.uid)
        && validUsersArray(request.resource.data)
        && request.resource.data.users == resource.data.users
        && request.resource.data.requestedBy == resource.data.requestedBy
        && request.resource.data.createdAt == resource.data.createdAt
        && request.resource.data.updatedAt is timestamp
        && friendshipTransitionAllowed();

      // Delete:
      // - requester can cancel pending
      // - other user can reject pending
      // - either can unfriend (ACCEPTED)
      // - blocker can unblock (BLOCKED_x)
      allow delete: if isSignedIn()
        && isParticipant(request.auth.uid)
        && (
          (resource.data.state == "PENDING") ||
          (resource.data.state == "ACCEPTED") ||
          (resource.data.state == "BLOCKED_0"
            && request.auth.uid == resource.data.users[0]) ||
          (resource.data.state == "BLOCKED_1"
            && request.auth.uid == resource.data.users[1])
        );
    }
    match /Meta/UserCounters {
      allow get: if isSignedIn();
      allow create, update: if isSignedIn()
        && request.resource.data.keys().hasOnly(['lastUserId'])
        && request.resource.data.lastUserId is int
        && (
          !resource.exists()
          ||
          (request.resource.data.lastUserId > resource.data.lastUserId)
        );

      allow delete: if false;
    }
  }
}

3. Realtime Database Setup

3.1 Create Realtime Database

In Firebase Console → Realtime Database:

  1. Create a new database in the same project.

  2. Choose a location (any supported region).

  3. You can start in locked mode and then paste the rules below.

3.2 Base Structure

The game will create nodes automatically when presence & invites are used, but you may create them upfront (empty) for clarity:

  • Root-level:

    • status (empty object)

    • invites (empty object)

    • chat is created automatically when global chat is used.

Example initial JSON:

{
  "status": {},
  "invites": {}
}

3.3 Realtime Database Rules

Paste the following into Realtime Database → Rules:

{
  "rules": {
    "status": {
      "$uid": {
        ".read": "auth != null && auth.uid == $uid",
        ".write": "auth != null && auth.uid == $uid"
      }
    },
    "invites": {
      "$targetUid": {
        ".read": "auth != null && auth.uid == $targetUid",
        "$inviteId": {
          ".write": "auth != null && newData.child('fromUid').val() === auth.uid"
        }
      }
    },
    "chat": {
      ".read": "auth != null",
      ".write": "false",
      ".indexOn": ["timestamp"],
      "$messageId": {
        ".write": "auth != null && !data.exists()",
        ".validate": "newData.hasChildren(['playerId', 'playerName', 'iconId', 'frameId', 'message', 'timestamp', 'isHighlighted'])",
        "playerId": {
          ".validate": "newData.isString() && newData.val() === auth.uid"
        },
        "playerName": {
          ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 50"
        },
        "iconId": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },
        "frameId": {
          ".validate": "newData.isString() && newData.val().length > 0"
        },
        "message": {
          ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length <= 500"
        },
        "timestamp": {
          ".validate": "newData.isNumber() && newData.val() <= now"
        },
        "isHighlighted": {
          ".validate": "newData.isBoolean()"
        }
      }
    }
  }
}

For developers who have not yet set up a project with Firebase or who have any questions, below is a detailed step-by-step guide with images:

2. Step‑by‑Step Guide (With Images)

2.1 Create a URP Project

  1. Open Unity Hub → New Project.

  2. Select Universal 3D (URP) template.

  3. Name your project and click Create.

Optional – HDRP/Built‑in pipelines work, but demo textures may need conversion.

2.2 Install Required Unity Packages

Window → Package Manager → Unity Registry. Add Input System, AI Navigation, In‑App Purchasing, 2D Sprite, TextMeshPro.

2.3 Import Firebase SDK

  1. Download lastest FirebaseAuth.unitypackage , FirebaseFirestore.unitypackage and FirebaseDatabase.unitypackage from the Firebase Unity SDK Archive.

  2. Double‑click each package to import.

  3. When prompted, allow ExternalDependancyManager to resolve libraries.

  4. add FIREBASE in Scripting Define Symbols in Build Settings > Player

2.4 Import the Template

Drag the BulletHell Elemental Template .unitypackage into the Editor or install via Package Manager → My Assets.

2.5 Configure Project Settings

  • Player Settings → Identification → Package Name must match the Android/iOS package names you register in Firebase.

  • Switch Platform to PC, Android or iOS before building (Editor → File → Build Settings).

2.6 Firebase Console Configuration

  1. Create Project → add Android and/or iOS app with the correct package name.

  2. Download google‑services.json (Android) or GoogleService‑Info.plist (iOS).

  3. Place json file in Assets/StreamingAssets/. If Unity cannot locate the file, verify the folder name and that the filename has no suffix (e.g., (1)).

  1. Firestore Database → Start in production mode.

  2. See the step-by-step instructions and rules at the beginning regarding which fields to create in Firestore.

  3. Apply the security rules (see Quick Start above).

2.8 Prepare Cloud Firestore

  • Authentication → Sign‑in method: enable Email/Password and Anonymous.

2.7 Enable Authentication

3. Realtime Database Setup

3.1 Create Realtime Database

In Firebase Console → Realtime Database:

  1. Create a new database in the same project.

  2. Choose a location (any supported region).

  3. You can start in locked mode and then paste the rules below.

3.2 Base Structure

The game will create nodes automatically when presence & invites are used, but you may create them upfront (empty) for clarity:

  • Root-level:

    • status (empty object)

    • invites (empty object)

    • chat is created automatically when global chat is used.

Example initial JSON (optional):

{
  "status": {},
  "invites": {}
}

2.9 Verify Tags, Layers & Scenes

If you imported the template into an existing project:

  • Add Monster tag in Edit → Project Settings → Tags & Layers.

  • Assign it to enemy prefabs in BulletHellTemplate/Resources/Monsters/.

  • Ensure scene order in Build Settings: Login, Home, others.

2.10 Test in Editor

  1. Open Login scene.

  2. Press Play – the template auto‑creates a local account, syncs with Firebase, and loads initial data.

  3. Check Console for Firebase initialization logs.

3. Common Issues & Fixes

Issue

Cause

Fix

google‑services.json not found

File in wrong location

Place inside Assets/StreamingAssets/. Refresh project.

"Unknown error" on account creation

Weak password

Use at least 8 chars, 1 number, 1 special char.

Android/iOS build fails on "Validate References"

Unused platform DLLs

Edit → Project Settings → Player → Other Settings, disable Validate References for unsupported platform.


4. Next Steps

  • Customize Battle Pass seasons by updating Season and StartSeason fields.

  • Harden security with App Check or move save logic to Cloud Functions (requires paid Firebase plan).

  • Join our community for support and feature requests.

Discord – https://discord.com/invite/EGGj77g3eQ Email – rafbizachi5@gmail.com

Last updated