Condividi tramite


Struttura dell'applicazione Marble Maze

La struttura di un'app UWP (Universal Windows Platform) DirectX differisce da quella di un'applicazione desktop tradizionale. Invece di usare tipi di handle come HWND e funzioni come CreateWindow, Windows Runtime fornisce interfacce come Windows::UI::Core::ICoreWindow in modo da poter sviluppare app UWP in modo più moderno e orientato agli oggetti. Questa sezione della documentazione illustra come è strutturato il codice dell'app Marble Maze.

Annotazioni

Il codice di esempio corrispondente a questo documento è disponibile nell'esempio di gioco DirectX Marble Maze .

Ecco alcuni dei punti chiave che questo documento illustra quando si struttura il codice del gioco:

  • Nella fase di inizializzazione configurare i componenti di runtime e libreria usati dal gioco e caricare risorse specifiche del gioco.
  • Le app UWP devono avviare l'elaborazione degli eventi entro 5 secondi dall'avvio. Caricare quindi solo le risorse essenziali quando si carica l'app. I giochi devono caricare risorse di grandi dimensioni in background e visualizzare una schermata di stato.
  • Nel ciclo di gioco, è necessario rispondere agli eventi di Windows, leggere l'input dell'utente, aggiornare gli oggetti della scena e renderizzare la scena.
  • Usare i gestori eventi per rispondere agli eventi della finestra. Questi sostituiscono i messaggi della finestra dalle applicazioni desktop di Windows.
  • Usare una macchina a stati per controllare il flusso e l'ordine della logica del gioco.

Organizzazione dei file

Alcuni componenti di Marble Maze possono essere riutilizzati con qualsiasi gioco con poche o nessuna modifica. Per il tuo gioco, puoi adattare l'organizzazione e le idee fornite da questi file. La tabella seguente descrive brevemente i file di codice sorgente importanti.

file Descrizione
App.h, App.cpp Definisce le classi app e DirectXApplicationSource, che incapsulano la vista (finestra, thread ed eventi) dell'app.
Audio.h, Audio.cpp Definisce la classe audio , che gestisce le risorse audio.
BasicLoader.h, BasicLoader.cpp Definisce la classe BasicLoader, che fornisce metodi di utilità per caricare trame, mesh e shader.
BasicMath.h Definisce strutture e funzioni che consentono di lavorare con dati vettoriali e matrici e calcoli. Molte di queste funzioni sono compatibili con i tipi di shader HLSL.
BasicReaderWriter.h, BasicReaderWriter.cpp Definisce la classe BasicReaderWriter, che usa Windows Runtime per leggere e scrivere dati di file in un'app UWP.
BasicShapes.h, BasicShapes.cpp Definisce la classe BasicShapes, che fornisce metodi di utilità per la creazione di forme di base, ad esempio cubi e sfere. Questi file non vengono usati dall'implementazione di Marble Maze.
Camera.h, Camera.cpp Definisce la classe Camera, che fornisce la posizione e l'orientamento di una fotocamera.
Collision.h, Collision.cpp Gestisce le informazioni sulle collisioni tra la biglia e altri oggetti, ad esempio il labirinto.
DDSTextureLoader.h, DDSTextureLoader.cpp Definisce la funzione CreateDDSTextureFromMemory, che carica texture in formato .dds da un buffer di memoria.
DirectXHelper.h Definisce le funzioni helper DirectX utili per molte app UWP DirectX.
LoadScreen.h, LoadScreen.cpp Definisce la classe LoadScreen che visualizza una schermata di caricamento durante l'inizializzazione dell'app.
MarbleMazeMain.h, MarbleMazeMain.cpp Definisce la classe MarbleMazeMain, che gestisce risorse specifiche del gioco e definisce gran parte della logica del gioco.
MediaStreamer.h, MediaStreamer.cpp Definisce la classe MediaStreamer, che usa Media Foundation per facilitare la gestione delle risorse audio del gioco.
PersistentState.h, PersistentState.cpp Definisce la classe PersistentState, che legge e scrive i tipi di dati primitivi da e in un archivio di backup.
Physics.h, Physics.cpp Definisce la classe Fisica, che implementa la simulazione fisica tra la biglia e il labirinto.
Primitives.h Definisce i tipi geometrici utilizzati dal gioco.
SampleOverlay.h, SampleOverlay.cpp Definisce la classe SampleOverlay, che fornisce dati e operazioni comuni dell'interfaccia utente e 2D.
SDKMesh.h, SDKMesh.cpp Definisce la classe SDKMesh, che carica e rende mesh nel formato SDK Mesh (.sdkmesh).
StepTimer.h Definisce la classe StepTimer, che offre un modo semplice per ottenere tempi totali e tempi trascorsi.
UserInterface.h, UserInterface.cpp Definisce le funzionalità correlate all'interfaccia utente, ad esempio il sistema di menu e la tabella dei punteggi elevati.

 

Formati di risorse in fase di progettazione e di runtime

Quando è possibile, usare formati di runtime anziché formati in fase di progettazione per caricare in modo più efficiente le risorse del gioco.

Un formato in fase di progettazione è il formato usato durante la progettazione della risorsa. In genere, i progettisti 3D lavorano con formati in fase di progettazione. Alcuni formati in fase di progettazione sono anche basati su testo, in modo da poterli modificare in qualsiasi editor basato su testo. I formati in fase di progettazione possono essere verbosi e contenere più informazioni rispetto alle esigenze del gioco. Un formato di tempo di esecuzione è il formato binario letto dal tuo gioco. I formati di runtime sono in genere più compatti ed efficienti da caricare rispetto ai formati di fase di progettazione corrispondenti. Ecco perché la maggior parte dei giochi usa asset di runtime in fase di esecuzione.

Anche se il gioco può leggere direttamente un formato in fase di progettazione, esistono diversi vantaggi nell'uso di un formato di runtime separato. Poiché i formati di runtime sono spesso più compatti, richiedono meno spazio su disco e richiedono meno tempo per essere trasferiti attraverso una rete. Inoltre, i formati di runtime vengono spesso rappresentati come strutture di dati mappate alla memoria. Pertanto, possono essere caricati in memoria molto più velocemente di, ad esempio, un file di testo basato su XML. Infine, poiché i formati di runtime separati sono in genere codificati in formato binario, sono più difficili per l'utente finale da modificare.

Gli shader HLSL sono un esempio di risorse che usano formati di fase di progettazione e di runtime diversi. Marble Maze usa .hlsl come formato in fase di progettazione e .cso come formato di runtime. Un file con estensione hlsl contiene il codice sorgente per lo shader; Un file con estensione cso contiene il codice di byte dello shader corrispondente. Quando converti i file con estensione hlsl offline e fornisci file con estensione cso con il gioco, evita la necessità di convertire i file di origine HLSL in codice byte quando il gioco viene caricato.

Per motivi di istruzioni, il progetto Marble Maze include sia il formato in fase di progettazione che il formato di runtime per molte risorse, ma devi solo mantenere i formati della fase di progettazione nel progetto di origine per il tuo gioco, perché puoi convertirli in formati di runtime quando ne hai bisogno. Questa documentazione illustra come convertire i formati in fase di progettazione nei formati di runtime.

Ciclo di vita dell'applicazione

Marble Maze segue il ciclo di vita di una tipica app UWP. Per altre info sul ciclo di vita di un'app UWP, vedi Ciclo di vita dell'app.

Quando un gioco UWP viene inizializzato, generalmente inizializza i componenti di runtime come Direct3D, Direct2D, oltre a eventuali librerie di input, audio o fisica che utilizza. Carica anche risorse specifiche del gioco necessarie prima dell'inizio del gioco. Questa inizializzazione si verifica una volta durante una sessione di gioco.

Dopo l'inizializzazione, i giochi in genere eseguono il ciclo del gioco . In questo ciclo, i giochi in genere eseguono quattro azioni: elaborare gli eventi di Windows, raccogliere input, aggiornare gli oggetti scena ed eseguire il rendering della scena. Quando il gioco aggiorna la scena, può applicare lo stato di input corrente agli oggetti scena e simulare eventi fisici, ad esempio collisioni di oggetti. Il gioco può anche eseguire altre attività, ad esempio la riproduzione di effetti sonori o l'invio di dati in rete. Quando il gioco esegue il rendering della scena, acquisisce lo stato corrente della scena e lo disegna sul dispositivo di visualizzazione. Le sezioni seguenti descrivono queste attività in modo più dettagliato.

Aggiunta al modello

Il modello di app DirectX 11 (Windows universale) crea una finestra core in cui è possibile eseguire il rendering con Direct3D. Il modello include anche la classe DeviceResources che crea tutte le risorse del dispositivo Direct3D necessarie per il rendering del contenuto 3D in un'app UWP.

La classe App crea l'oggetto della classe MarbleMazeMain, avvia il caricamento delle risorse, cicla per aggiornare il timer e chiama il metodo MarbleMazeMain::Render a ogni fotogramma. I metodi App::OnWindowSizeChanged, App::OnDpiChangede App::OnOrientationChanged chiamano ogni metodo MarbleMazeMain::CreateWindowSizeDependentResources, e il metodo App::Run chiama i metodi MarbleMazeMain::Update e MarbleMazeMain::Render.

L'esempio seguente mostra dove il metodo App::SetWindow crea l'oggetto classe MarbleMazeMain. La classe DeviceResources viene passata al metodo in modo che possa usare gli oggetti Direct3D per il rendering.

    m_main = std::unique_ptr<MarbleMazeMain>(new MarbleMazeMain(m_deviceResources));

La classe App avvia anche il caricamento delle risorse posticipate per il gioco. Per altri dettagli, vedere la sezione successiva.

Inoltre, la classe App configura i gestori di eventi per gli eventi CoreWindow. Quando i gestori di questi eventi vengono chiamati, essi passano l'input alla classe MarbleMazeMain.

Caricamento degli asset del gioco in sottofondo

Per assicurarsi che il gioco possa rispondere agli eventi della finestra entro 5 secondi dall'avvio, ti consigliamo di caricare gli asset del gioco in modo asincrono o in background. Man mano che gli asset si caricano in background, il gioco può rispondere agli eventi della finestra.

Annotazioni

È anche possibile visualizzare il menu principale quando è pronto e consentire agli asset rimanenti di continuare il caricamento in background. Se l'utente seleziona un'opzione dal menu prima del caricamento di tutte le risorse, è possibile indicare che le risorse della scena continuano a essere caricate visualizzando ad esempio un indicatore di stato.

 

Anche se il gioco contiene relativamente pochi asset di gioco, è consigliabile caricarli in modo asincrono per due motivi. Un motivo è che è difficile garantire che tutte le risorse caricheranno rapidamente in tutti i dispositivi e tutte le configurazioni. Inoltre, incorporando il caricamento asincrono in anticipo, il codice è pronto per la scalabilità man mano che si aggiungono funzionalità.

Il caricamento asincrono degli asset inizia con il metodo App::Load. Questo metodo usa l'attività classe per caricare gli asset del gioco in background.

    task<void>([=]()
    {
        m_main->LoadDeferredResources(true, false);
    });

La classe MarbleMazeMain definisce il flag m_deferredResourcesReady per indicare il completamento del caricamento asincrono. Il metodo MarbleMazeMain::LoadDeferredResources carica le risorse del gioco e quindi imposta questo flag. L'aggiornamento (MarbleMazeMain::Update) e la fase di rendering (MarbleMazeMain::Render) dell'app controllano questo flag. Quando questo flag è impostato, il gioco continua come di consueto. Se il flag non è ancora impostato, il gioco visualizza la schermata di caricamento.

Per altre informazioni sulla programmazione asincrona per le app UWP, vedere Programmazione asincrona in C++.

Suggerimento

Se stai scrivendo codice di gioco che fa parte di una libreria C++ di Windows Runtime (in altre parole, una DLL), valuta se leggere Creazione di operazioni asincrone in C++ per le app UWP per imparare a creare operazioni asincrone che possono essere utilizzate dalle app e da altre librerie.

 

Il ciclo del gioco

Il metodo App::Run esegue il ciclo principale del gioco (MarbleMazeMain::Update). Questo metodo viene chiamato ogni frame.

Per separare la visualizzazione e il codice della finestra dal codice specifico del gioco, abbiamo implementato il metodo App::Run per inoltrare le chiamate di aggiornamento e rendering all'oggetto MarbleMazeMain.

L'esempio seguente mostra il metodo App::Run, che include il ciclo principale del gioco. Il ciclo del gioco aggiorna le variabili di tempo totale e tempo per fotogramma, quindi aggiorna ed esegue il rendering della scena. Ciò garantisce anche che il rendering del contenuto venga eseguito solo quando la finestra è visibile.

void App::Run()
{
    while (!m_windowClosed)
    {
        if (m_windowVisible)
        {
            CoreWindow::GetForCurrentThread()->Dispatcher->
                ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);

            m_main->Update();

            if (m_main->Render())
            {
                m_deviceResources->Present();
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread()->Dispatcher->
                ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }

    // The app is exiting so do the same thing as if the app were being suspended.
    m_main->OnSuspending();

#ifdef _DEBUG
    // Dump debug info when exiting.
    DumpD3DDebug();
#endif //_DEGBUG
}

Macchina a stati finiti

I giochi in genere contengono una macchina a stati (nota anche come macchina a stati finiti, o FSM) per controllare il flusso e l'ordine della logica del gioco. Una macchina a stati contiene un determinato numero di stati e la possibilità di eseguire la transizione tra di esse. Una macchina a stati in genere inizia da uno stato iniziale , passa a uno o più stati intermedi ed eventualmente termina in uno stato terminale .

Un ciclo di gioco usa spesso una macchina a stati così da poter eseguire la logica specifica dello stato del gioco corrente. Marble Maze definisce l'enumerazione GameState, che definisce ogni possibile stato del gioco.

enum class GameState
{
    Initial,
    MainMenu,
    HighScoreDisplay,
    PreGameCountdown,
    InGameActive,
    InGamePaused,
    PostGameResults,
};

Lo stato MainMenu, ad esempio, definisce che viene visualizzato il menu principale e che il gioco non è attivo. Viceversa, lo stato InGameActive definisce che il gioco è attivo e che il menu non viene visualizzato. La classe MarbleMazeMain definisce la variabile membro m_gameState per mantenere lo stato attivo del gioco.

I metodi MarbleMazeMain::Update e MarbleMazeMain::Render usano istruzioni switch per eseguire la logica per lo stato corrente. L'esempio seguente mostra l'aspetto di un'istruzione switch per il metodo MarbleMazeMain::Update (i dettagli vengono rimossi per illustrare la struttura).

switch (m_gameState)
{
case GameState::MainMenu:
    // Do something with the main menu. 
    break;

case GameState::HighScoreDisplay:
    // Do something with the high-score table. 
    break;

case GameState::PostGameResults:
    // Do something with the game results. 
    break;

case GameState::InGamePaused:
    // Handle the paused state. 
    break;
}

Quando la logica o il rendering del gioco dipende da uno stato specifico del gioco, lo sottolineiamo in questa documentazione.

Gestione di eventi di app e finestre

Windows Runtime fornisce un sistema di gestione degli eventi orientato agli oggetti in modo da poter gestire più facilmente i messaggi di Windows. Per utilizzare un evento in un'applicazione, è necessario fornire un gestore eventi o un metodo di gestione degli eventi che risponde all'evento. È anche necessario registrare il gestore eventi con l'origine dell'evento. Questo processo viene spesso definito collegamento degli eventi.

Supporto di sospensione, ripresa e riavvio

Marble Maze viene sospeso quando l'utente si allontana da esso o quando Windows entra in uno stato di basso consumo. Il gioco viene ripreso quando l'utente lo sposta in primo piano o quando Windows esce da uno stato di basso consumo. In genere, non si chiudono le app. Windows può terminare l'app quando si trova nello stato sospeso e Windows richiede le risorse, ad esempio la memoria, che l'app usa. Windows invia una notifica a un'app quando sta per essere sospesa o ripresa, ma non invia una notifica all'app quando sta per essere terminata. Pertanto, l'app deve essere in grado di salvare, al momento in cui Windows notifica all'app che sta per essere sospesa, tutti i dati che sarebbero necessari per ripristinare lo stato utente corrente al riavvio dell'app. Se la tua app ha uno stato dell'utente significativo che è costoso da salvare, potrebbe essere necessario salvare regolarmente lo stato, persino prima che l'app riceva la notifica di sospensione. Marble Maze risponde alle notifiche di sospensione e ripresa per due motivi:

  1. Quando l'app viene sospesa, il gioco salva lo stato corrente del gioco e mette in pausa la riproduzione audio. Quando l'app viene riaperta, il gioco riprende la riproduzione audio.
  2. Quando l'app viene chiusa e riavviata in un secondo momento, il gioco riprende dallo stato precedente.

Marble Maze esegue le attività seguenti per supportare la sospensione e la ripresa:

  • Salva lo stato nell'archiviazione permanente nei punti chiave del gioco, ad esempio quando l'utente raggiunge un checkpoint.
  • Risponde alle notifiche di sospensione salvandone lo stato nell'archiviazione permanente.
  • Risponde alla ripresa delle notifiche caricandone lo stato dall'archiviazione permanente. Carica anche lo stato precedente durante l'avvio.

Per supportare la sospensione e la ripresa, Marble Maze definisce la classe PersistentState. Vedere PersistentState.h e PersistentState.cpp). Questa classe usa l'interfaccia Windows::Foundation::Collections::IPropertySet per leggere e scrivere proprietà. La classe PersistentState fornisce metodi che leggono e scrivono tipi di dati primitivi ( ad esempio bool, int, float, XMFLOAT3e Platform::String), da e a un archivio di backup.

ref class PersistentState
{
internal:
    void Initialize(
        _In_ Windows::Foundation::Collections::IPropertySet^ settingsValues,
        _In_ Platform::String^ key
        );

    void SaveBool(Platform::String^ key, bool value);
    void SaveInt32(Platform::String^ key, int value);
    void SaveSingle(Platform::String^ key, float value);
    void SaveXMFLOAT3(Platform::String^ key, DirectX::XMFLOAT3 value);
    void SaveString(Platform::String^ key, Platform::String^ string);

    bool LoadBool(Platform::String^ key, bool defaultValue);
    int  LoadInt32(Platform::String^ key, int defaultValue);
    float LoadSingle(Platform::String^ key, float defaultValue);

    DirectX::XMFLOAT3 LoadXMFLOAT3(
        Platform::String^ key, 
        DirectX::XMFLOAT3 defaultValue);

    Platform::String^ LoadString(
        Platform::String^ key, 
        Platform::String^ defaultValue);

private:
    Platform::String^ m_keyName;
    Windows::Foundation::Collections::IPropertySet^ m_settingsValues;
};

La classe MarbleMazeMain contiene un oggetto PersistentState . Il costruttore MarbleMazeMain inizializza questo oggetto e fornisce l'archivio dati dell'applicazione locale come archivio dati di backup.

m_persistentState = ref new PersistentState();

m_persistentState->Initialize(
    Windows::Storage::ApplicationData::Current->LocalSettings->Values,
    "MarbleMaze");

Marble Maze salva il suo stato quando la biglia supera un checkpoint o l'obiettivo (nel metodo MarbleMazeMain::Update) e quando la finestra perde il focus (nel metodo MarbleMazeMain::OnFocusChange). Se il gioco contiene una grande quantità di dati sullo stato, ti consigliamo di salvare occasionalmente lo stato nell'archiviazione permanente in modo simile perché hai solo pochi secondi per rispondere alla notifica di sospensione. Pertanto, quando l'app riceve una notifica di sospensione, deve salvare solo i dati sullo stato modificati.

Per rispondere alle notifiche di sospensione e ripresa, la classe MarbleMazeMain definisce i metodi SaveState e LoadState chiamati in sospensione e ripresa. Il metodo MarbleMazeMain::OnSuspending gestisce l'evento di sospensione e il metodo MarbleMazeMain::OnResuming gestisce l'evento di ripresa.

Il metodo MarbleMazeMain::OnSuspending salva lo stato del gioco e sospende l'audio.

void MarbleMazeMain::OnSuspending()
{
    SaveState();
    m_audio.SuspendAudio();
}

Il metodo MarbleMazeMain::SaveState salva i valori dello stato del gioco, ad esempio la posizione e la velocità correnti della biglia, il checkpoint più recente e la tabella dei punteggi più alti.

void MarbleMazeMain::SaveState()
{
    m_persistentState->SaveXMFLOAT3(":Position", m_physics.GetPosition());
    m_persistentState->SaveXMFLOAT3(":Velocity", m_physics.GetVelocity());

    m_persistentState->SaveSingle(
        ":ElapsedTime", 
        m_inGameStopwatchTimer.GetElapsedTime());

    m_persistentState->SaveInt32(":GameState", static_cast<int>(m_gameState));
    m_persistentState->SaveInt32(":Checkpoint", static_cast<int>(m_currentCheckpoint));

    int i = 0;
    HighScoreEntries entries = m_highScoreTable.GetEntries();
    const int bufferLength = 16;
    char16 str[bufferLength];

    m_persistentState->SaveInt32(":ScoreCount", static_cast<int>(entries.size()));

    for (auto iter = entries.begin(); iter != entries.end(); ++iter)
    {
        int len = swprintf_s(str, bufferLength, L"%d", i++);
        Platform::String^ string = ref new Platform::String(str, len);

        m_persistentState->SaveSingle(
            Platform::String::Concat(":ScoreTime", string), 
            iter->elapsedTime);

        m_persistentState->SaveString(
            Platform::String::Concat(":ScoreTag", string), 
            iter->tag);
    }
}

Quando il gioco riprende, deve riprendere soltanto l'audio. Non è necessario caricare lo stato dall'archiviazione permanente perché lo stato è già caricato in memoria.

Il modo in cui il gioco sospende e riprende l'audio viene illustrato nel documento Aggiunta di audio all'esempio Marble Maze.

Per supportare il riavvio, il costruttore MarbleMazeMain, chiamato durante l'avvio, invoca il metodo MarbleMazeMain::LoadState. Il metodo MarbleMazeMain::LoadState legge e applica lo stato agli oggetti gioco. Questo metodo imposta anche lo stato corrente del gioco su sospeso se il gioco è stato sospeso o attivo quando è stato sospeso. Sospendiamo il gioco in modo che l'utente non sia sorpreso dall'attività imprevista. Si sposta anche nel menu principale se il gioco non era in uno stato di gioco quando è stato sospeso.

void MarbleMazeMain::LoadState()
{
    XMFLOAT3 position = m_persistentState->LoadXMFLOAT3(
        ":Position", 
        m_physics.GetPosition());

    XMFLOAT3 velocity = m_persistentState->LoadXMFLOAT3(
        ":Velocity", 
        m_physics.GetVelocity());

    float elapsedTime = m_persistentState->LoadSingle(":ElapsedTime", 0.0f);

    int gameState = m_persistentState->LoadInt32(
        ":GameState", 
        static_cast<int>(m_gameState));

    int currentCheckpoint = m_persistentState->LoadInt32(
        ":Checkpoint", 
        static_cast<int>(m_currentCheckpoint));

    switch (static_cast<GameState>(gameState))
    {
    case GameState::Initial:
        break;

    case GameState::MainMenu:
    case GameState::HighScoreDisplay:
    case GameState::PreGameCountdown:
    case GameState::PostGameResults:
        SetGameState(GameState::MainMenu);
        break;

    case GameState::InGameActive:
    case GameState::InGamePaused:
        m_inGameStopwatchTimer.SetVisible(true);
        m_inGameStopwatchTimer.SetElapsedTime(elapsedTime);
        m_physics.SetPosition(position);
        m_physics.SetVelocity(velocity);
        m_currentCheckpoint = currentCheckpoint;
        SetGameState(GameState::InGamePaused);
        break;
    }

    int count = m_persistentState->LoadInt32(":ScoreCount", 0);

    const int bufferLength = 16;
    char16 str[bufferLength];

    for (int i = 0; i < count; i++)
    {
        HighScoreEntry entry;
        int len = swprintf_s(str, bufferLength, L"%d", i);
        Platform::String^ string = ref new Platform::String(str, len);

        entry.elapsedTime = m_persistentState->LoadSingle(
            Platform::String::Concat(":ScoreTime", string), 
            0.0f);

        entry.tag = m_persistentState->LoadString(
            Platform::String::Concat(":ScoreTag", string), 
            L"");

        m_highScoreTable.AddScoreToTable(entry);
    }
}

Importante

Marble Maze non distingue tra l'avvio a freddo, ovvero l'avvio per la prima volta senza un evento di sospensione precedente, e la ripresa da uno stato sospeso. Questa è la progettazione consigliata per tutte le app UWP.

Per altre informazioni sui dati dell'applicazione, vedi Memorizza e recupera le impostazioni e altri dati dell'app.

Passaggi successivi

Leggi Aggiungere contenuti visivi all'esempio di Marble Maze per informazioni su alcune delle procedure principali da tenere presenti quando lavori con le risorse visive.