overview

o hive.art transforma o tempo de espera passivo em uma experiência social ativa. a dinâmica do jogo começa com o sorteio de líderes, as 'Abelhas-Rainhas', que formam equipes (colmeias) com as pessoas ao redor usando códigos de emparelhamento exibidos na Apple TV.

o objetivo é puramente colaborativo: as colmeias recebem um tema surpresa — como 'Girafa' ou 'Espaço' — e cada jogador desenha uma parte da imagem em seu próprio iPhone. desse modo, múltiplas pessoas podem colaborar em uma produção artística coletiva, que ajuda a ressignificar o tempo de espera, fortalecer vínculos entre as pessoas em diferentes espaços e estimular a criatividade coletiva.

tecnicamente, o projeto viabiliza essa colaboração instantânea através de uma arquitetura modular compartilhada, representada por um target chamado HiveArtShared, e sincronização via Firebase Firestore. o uso do PencilKit permite a transmissão de vetores de desenho complexos em tempo real, garantindo que o traço feito no iPhone apareça instantaneamente em outros dispositivos.

o sistema gerencia estados de jogo concorrentes para manter a fluidez entre os múltiplos dispositivos da 'colmeia', assegurando que a experiência criativa seja contínua e imersiva para todos os participantes.

design choices

palette

honey #FF9500
flora #9CD579
bloom #A61825
swarm #2E2E2E

typography

Monley / logo primário
Zebras jogam xadrez com o velho faquir
SF Pro Rounded / interface e texto
Zebras jogam xadrez com o velho faquir

some fonts used in this project are proprietary and may not display correctly if they are not installed on your system.

rationale

o design do hive.art foi cuidadosamente elaborado para refletir a temática de colaboração e criatividade do jogo aliada à ideia de abelhas em uma colmeia. a paleta de cores vibrantes, com tons de amarelo mel, verde, vermelho e preto, remete à natureza das abelhas e à energia dinâmica do processo criativo coletivo.

a tipografia escolhida, com fontes arredondadas e amigáveis, visa criar uma atmosfera acolhedora e acessível para os jogadores de todas as idades. a escolha pela SF Pro Rounded também tem um motivo técnico: considerando seu uso nativo nas plataformas Apple e sua legibilidade, a SF Pro facilita a navegação entre os dispositivos Apple TV e iPhone, garantindo uma experiência mais fluida.

o hive.art conta ainda com uma construção visual inspirada em hexágonos, a forma natural assumida pelas colmeias, além de detalhes gráficos que remetem ao universo das abelhas, como pequenas ilustrações e termos como abelha-rainha e colmeia. esses detalhes visuais e tipográficos reforçam a identidade do jogo e proporcionam uma experiência imersiva, conectando os jogadores à temática central enquanto colaboram na criação artística.

tech stack

SwiftUI
interface e lógica de jogo
tvOS
plataforma de exibição compartilhada
PencilKit
desenho em tempo real
Firebase
sincronização de dados
Combine
gerenciamento de estado reativo

code snippets

Forma Hexagonal

swift
struct Hexagon: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        let center = CGPoint(x: rect.midX, y: rect.midY)
        let radius = min(rect.size.height, rect.size.width) / 2
        let corners = corners(center: center, radius: radius)
        path.move(to: corners[0])
        corners[1...5].forEach() { point in
            path.addLine(to: point)
        }
        path.closeSubpath()
        return path
    }
    
    func corners(center: CGPoint, radius: CGFloat) -> [CGPoint] {
        var points: [CGPoint] = []
        for i in (0...5) {
            let angle = CGFloat.pi / 3 * CGFloat(i)
            let point = CGPoint(
                x: center.x + radius * cos(angle),
                y: center.y + radius * sin(angle)
            )
            points.append(point)
        }
        return points
    }
}

Vista do Coordenador

swift
struct CoordinatorView<C: CoordinatorViewable>: View {
    @ObservedObject var coordinator: C
    var body: some View {
        NavigationStack(path: $coordinator.navigationPath) {
            coordinator.getInitialPage().build(coordinator: coordinator)
                .navigationDestination(for: C.PageType.self) { page in
                    page.build(coordinator: coordinator)
                }
        }
        .sheet(item: $coordinator.sheet) { item in
            item.page.build(coordinator: coordinator)
        }
        .fullScreenCover(item: $coordinator.fullScreenCover) { item in
            item.page.build(coordinator: coordinator)
        }
        .onAppear {
            coordinator.start()
        }
    }
}

ViewModel Concorrente

swift
class DrawingSectionViewModel: ObservableObject {
    private(set) var coordinator: UIDrawingSectionViewCoordinator
    private var drawingService: any DrawingServiceProtocol
    
    init(drawing: UUID, drawingService: any DrawingServiceProtocol = RealtimeDBDrawingService()) {
        coordinator = .init()
        self.drawingService = drawingService
        Task { @MainActor in
            do {
                try await drawingService.subscribeToDrawing(id: drawing)
            } catch {
                print("Failed to subscribe to drawing \(drawing): \(error)")
            }
        }
    }
    
    private func onDrawingDidChange(_ drawing: PKDrawing) {
        Task { @MainActor in
            do {
                try await drawingService.updateDrawingData(newData: drawing)
            } catch {
                print("Error updating drawing: \(error)")
            }
        }
    }
}

Serviço Firestore

swift
public final class FirestoreDatabaseService<T: Codable>: DatabaseServiceProtocol {
    public typealias Model = T
    private let db = Firestore.firestore()
    private var listeners: [UUID: ListenerRegistration] = [:]
    
    public func addListener(for collection: String, using closure: @escaping ([T], (any Error)?) -> Void) async -> UUID {
        let id = UUID()
        let listener = db.collection(collection).addSnapshotListener({ snapshot, error in
            guard let snapshot else {
                closure([], error)
                return
            }
            let documents = snapshot.documents.compactMap{ doc -> T? in
                try? doc.data(as: T.self)
            }
            closure(documents, error)
        })
        listeners[id] = listener
        return id
    }
    
    public func addDocument(_ document: Model, to collection: String) async throws {
        do {
            try db.collection(collection).addDocument(from: document)
        } catch {
            NSLog("Error fetching documents from collection '(collection)': (error.localizedDescription)")
            throw error
        }
    }
}

Processamento de Fila de Eventos

swift
final class RealtimeDBEventService: EventServiceProtocol {
    private let db: any DatabaseServiceProtocol<any Event>
    private let collectionPath: String
    func addEventListener(completion: @escaping (any Event) -> Void) async throws -> UUID {
        await db.addListener(for: collectionPath, using: { events, error in
            guard error == nil else { return }
            Task { @MainActor in
                for event in events {
                    completion(event)
                    try await self.db.deleteDocument(event.id.uuidString, from: self.collectionPath)
                }
            }
        })
    }
}

Orquestração Multiplayer

swift
class GameManager {
    func handle(event: any Event) {
        switch event {
            case let e as PlayerJoinRoomEvent:
                handlePlayerJoinRoomEvent(e)
            case let e as PlayerLeaveRoomEvent:
                handlePlayerLeaveRoomEvent(e)
            case let e as PlayerJoinHiveEvent:
                handlePlayerJoinHiveEvent(e)
            case let e as PlayerLeaveHiveEvent:
                handlePlayerLeaveHiveEvent(e)
            case let e as RoomEndStepEvent:
                handleRoomEndStepEvent(e)
            default:
                print("Unhandled event: \(event)")
        }
    }
    func handleRoomEndStepEvent(_ event: RoomEndStepEvent) {
        Task { @MainActor in
            switch event.step {
                case .joining:
                    try await handleThemeReveal()
                case .themeReveal:
                    try await handleLeaderReveal()
                case .leaderReveal:
                    try await roomService.changeStep(to: .hivePairing)
                default:
                    try await roomService.changeStep(to: .end)
            }
        }
    }
}

credits

people