Come codificare i risultati sbloccabili per il tuo gioco (un approccio semplice)

I risultati sono estremamente popolari tra i giocatori. Possono essere utilizzati in vari modi, dall'insegnamento alla misurazione dei progressi, ma come possiamo codificarli? In questo tutorial presenterò un approccio semplice per l'implementazione dei risultati.

Nota: Sebbene questo tutorial sia scritto usando AS3 e Flash, dovresti essere in grado di utilizzare le stesse tecniche e concetti in quasi tutti gli ambienti di sviluppo di giochi.

Puoi scaricare o forgiare il codice finale dal repository GitHub: https://github.com/Dovyski/Achieve


Codice di Achivements: Dolcetto o scherzetto?

A prima vista, la programmazione di un sistema di risultati sembra banale, e questo è parzialmente vero. Generalmente vengono implementati con i contatori, ognuno dei quali rappresenta un'importante metrica di gioco, come il numero di nemici uccisi o la vita del giocatore.

Un risultato è sbloccato se quei contatori corrispondono a test specifici:

 killedEnemies = killedEnemies + 1; if (killedEnemies> = 10 && lives> = 2) // sblocca risultato

Non c'è niente di sbagliato in questo approccio, ma immagina un test con 10 o più contatori. A seconda del numero di risultati (e contatori), si può finire con il codice spaghetti replicato dappertutto:

 se (ucciso nemici> 5 && lives> 2 && deaths <= 3 && perfectHits > 20 && ammo> = 100 && wrongHits <= 1)  // unlock achievement 

Un'idea migliore

Il mio approccio si basa anche sui contatori, ma sono controllati da semplici regole:


Risultati basati sulle proprietà. I rettangoli grigi sono proprietà / risultati attivi.

Un risultato è sbloccato quando tutte le sue proprietà correlate sono attive. Un risultato può avere una o più proprietà correlate, ognuna gestita da una singola classe, quindi non c'è bisogno di scrivere Se() dichiarazioni in tutto il codice.

Ecco l'idea:

  • Identifica qualsiasi metrica di gioco interessante (uccisioni, morti, errori, partite, ecc.).
  • Ogni metrica diventa a proprietà, guidato da un vincolo di aggiornamento. Il vincolo controlla se la proprietà deve essere cambiata quando arriva un nuovo valore.
  • I vincoli sono: "aggiorna solo se il nuovo valore è maggiore del valore corrente"; "aggiorna solo se il nuovo valore è inferiore al valore corrente"; e "aggiorna indipendentemente dal valore corrente".
  • Ogni proprietà ha un Attivazione regola - ad esempio "uccide è attivo se il suo valore è maggiore di 10".
  • Controlla periodicamente le proprietà attivate. Se tutte le proprietà correlate di un risultato sono attive, il risultato è sbloccato.

Al fine di implementare un risultato, è necessario definire quali proprietà dovrebbero essere attive per sbloccare tale risultato. Dopo che le proprietà devono essere aggiornate durante il gameplay, e il gioco è fatto!

Le prossime sezioni presentano un'implementazione per quell'idea.


Descrivere proprietà e risultati

Il primo passo di implementazione è la rappresentazione di proprietà e realizzazioni. La classe Proprietà può essere il seguente:

 Proprietà di classe pubblica private var mName: String; private var mValue: int; private var mActivation: String; private var mActivationValue: int; private var mInitialValue: int; proprietà public function (theName: String, theInitialValue: int, theActivation: String, theActivationValue: int) mName = theName; mActivation = theActivation; mActivationValue = theActivationValue; mInitialValue = theInitialValue; 

Una proprietà ha un nome (MNAME), un valore (Lvalore, che è il contatore), un valore di attivazione (mActivationValue) e una regola di attivazione (mActivation).

La regola di attivazione è qualcosa come "attivo se maggiore di" e controlla se una proprietà è attiva (ne parleremo più avanti). Si dice che una proprietà sia attiva quando il suo valore viene confrontato con il valore di attivazione e il risultato soddisfa la regola di attivazione.

Un risultato può essere descritto come segue:

 public class Achievement private var mName: String; // achievement name private var mProps: Array; // array di proprietà correlate private var mUnlocked: Boolean; // achievement è sbloccato o non public Achievement (theId: String, theRelatedProps: Array) mName = theId; mProps = theRelatedProps; mUnlocked = false; 

Un risultato ha un nome (MNAME) e una bandiera per indicare se è già sbloccato (mUnlocked). L'array mProps contiene una voce per ogni proprietà necessaria per sbloccare il risultato. Quando tutte queste proprietà sono attive, il risultato dovrebbe essere sbloccato.


Gestione delle proprietà e dei risultati

Tutte le proprietà e i risultati saranno gestiti da una classe centralizzata denominata Raggiungere. Questa classe dovrebbe comportarsi come una scatola nera che riceve aggiornamenti di proprietà e indica se un risultato è stato sbloccato. La sua struttura di base è:

 public class Achieve // regole di attivazione public static const ACTIVE_IF_GREATER_THAN: String = ">"; public static const ACTIVE_IF_LESS_THAN: String = "<"; public static const ACTIVE_IF_EQUALS_TO :String = "=="; private var mProps :Object; // dictionary of properties private var mAchievements :Object; // dictionary of achievements public function Achieve()  mProps =  ; mAchievements =  ;  

Poiché tutte le proprietà verranno aggiornate utilizzando il loro nome come indice di ricerca, è conveniente memorizzarle in un dizionario (il file mProps attributo nella classe). I risultati saranno gestiti allo stesso modo, quindi vengono memorizzati allo stesso modo (mAchievements attributo).

Per gestire l'aggiunta di proprietà e risultati, creiamo i metodi defineProperty () e defineAchievement ():

 public function defineProperty (theName: String, theInitialValue: int, theaActivationMode: String, theValue: int): void mProps [theName] = new Property (theName, theInitialValue, theaActivationMode, theValue);  public function defineAchievement (theName: String, theRelatedProps: Array): void mAchievements [theName] = new Achievement (theName, theRelatedProps); 

Entrambi i metodi aggiungono semplicemente una voce alla proprietà o al dizionario degli obiettivi.


Aggiornamento delle proprietà

Ora che il Raggiungere la classe può gestire proprietà e risultati, è ora di renderla capace di aggiornare i valori delle proprietà. Una proprietà verrà aggiornata durante il gioco e fungerà da contatore. Ad esempio la proprietà killedEnemies dovrebbe essere incrementato ogni volta che un nemico viene distrutto.

Sono necessari solo due metodi: uno per leggere e un altro per impostare un valore di proprietà. Entrambi i metodi appartengono al Raggiungere classe e può essere implementato come segue:

 funzione pubblica getValue (theProp: String): int return mProps [theProp] .value;  funzione privata setValue (theProp: String, theValue: int): void mProps [theProp] .value = theValue; 

È anche utile disporre di un metodo per aggiungere un valore a un gruppo di proprietà, ad esempio un incremento / decremento batch:

 funzione pubblica addValue (theProps: Array, theValue: int): void for (var i: int = 0; i < theProps.length; i++)  var aPropName :String = theProps[i]; setValue(aPropName, getValue(aPropName) + theValue);  

Verifica dei risultati

Controllare i risultati sbloccati è semplice e facile: scorrere il dizionario dei risultati, controllando se tutte le proprietà correlate di un risultato sono attive.

Per eseguire questa iterazione, per prima cosa abbiamo bisogno di un metodo per verificare se una proprietà è attiva:

 proprietà public class // // il resto del codice classe è stato omesso ... // public function isActive (): Boolean var aRet: Boolean = false; switch (mActivation) case Achieve.ACTIVE_IF_GREATER_THAN: aRet = mValue> mActivationValue; rompere; case Achieve.ACTIVE_IF_LESS_THAN: aRet = mValue < mActivationValue; break; case Achieve.ACTIVE_IF_EQUALS_TO: aRet = mValue == mActivationValue; break;  return aRet;  

Ora implementiamo il checkAchievements () metodo nel Raggiungere classe:

 public function checkAchievements (): Vector var aRet: Vector = new Vector (); for (var n: String in mAchievements) var aAchivement: Achievement = mAchievements [n]; if (aAchivement.unlocked == false) var aActiveProps: int = 0; per (var p: int = 0; p < aAchivement.props.length; p++)  var aProp :Property = mProps[aAchivement.props[p]]; if (aProp.isActive())  aActiveProps++;   if (aActiveProps == aAchivement.props.length)  aAchivement.unlocked = true; aRet.push(aAchivement);    return aRet; 

Il checkAchievements () il metodo itera su tutti i risultati. Alla fine di ogni iterazione verifica se il numero di proprietà attive per quel particolare risultato è uguale alla quantità di proprietà correlate. Se ciò è vero, allora il 100% delle proprietà correlate per quel risultato sono attive, quindi il giocatore ha sbloccato un nuovo risultato.

Per comodità, il metodo restituisce a Vettore (che agisce come una matrice o lista tipizzata) contenente tutti i risultati sbloccati durante il controllo. Inoltre, il metodo contrassegna tutti i risultati trovati come "sbloccati", quindi non verranno più analizzati in futuro.


Aggiunta di vincoli alle proprietà

Finora le proprietà non hanno vincoli, il che significa che qualsiasi valore è passato valore impostato() aggiornerà la proprietà. Immagina una proprietà chiamata killedWithASingleBomb, che memorizza il numero di nemici uccisi dal giocatore usando una singola bomba.

Se la sua regola di attivazione è "se maggiore di 5" e il giocatore uccide sei nemici, dovrebbe sbloccare il risultato. Tuttavia, assumere il checkAchievements () il metodo non è stato invocato subito dopo l'esplosione della bomba. Se il giocatore fa esplodere un'altra bomba e uccide tre nemici, la proprietà sarà aggiornata a 3.

Questo cambiamento farà perdere al giocatore il risultato. Per risolvere il problema, possiamo usare la regola di attivazione della proprietà come a costrizione. Significa che una proprietà con "se maggiore di 5" verrà aggiornata solo se il nuovo valore è maggiore di quello corrente:

 funzione privata setValue (theProp: String, theValue: int): void // Quale regola di attivazione? switch (mProps [theProp] .activation) case Achieve.ACTIVE_IF_GREATER_THAN: theValue = theValue> mProps [theProp] .value? theValue: mProps [theProp] .value; rompere; case Achieve.ACTIVE_IF_LESS_THAN: theValue = theValue < mProps[theProp].value ? theValue : mProps[theProp].value; break;  mProps[theProp].value = theValue; 

Ripristino e tagging delle proprietà

Spesso i risultati non sono legati all'intero gioco, ma specifici a periodi come i livelli. Qualcosa come "sconfiggere un livello uccidendo 40 o più nemici" deve essere contato durante il livello, quindi resettare in modo che il giocatore possa riprovare al livello successivo.

Una possibile soluzione per questo problema è l'aggiunta di tag alle proprietà. L'uso di tag consente la manipolazione di un gruppo di proprietà. Utilizzando l'esempio precedente, il killedEnemies la proprietà può essere taggata come levelStuff, per esempio.

Di conseguenza, è possibile verificare i risultati e ripristinare le proprietà in base ai tag:

 // Definire la proprietà usando un tag defineProperty ("killedEnemies", ..., "levelStuff"); if (levelIsOver ()) // Controlla i risultati, ma solo quelli basati sulle proprietà // taggati con "levelStuff". Tutte le altre proprietà saranno ignorate, // quindi non interferirà con altri risultati. checkAchievements ( "levelStuff"); // Ripristina tutte le proprietà taggate con 'levelStuff' resetProperties ("levelStuff"); 

Il metodo checkAchievements () diventa molto più versatile con i tag. Può essere invocato in qualsiasi momento ora, purché gestisca il gruppo corretto di proprietà.


Dimostrazione di utilizzo

Di seguito è riportato uno snippet di codice che illustra come utilizzare questa implementazione del risultato:

 var a: Achieve = new Achieve (); function initGame (): void a.defineProperty ("killedEnemies", 0, Achieve.ACTIVE_IF_GREATER_THAN, 10, "levelStuff"); a.defineProperty ("lives", 3, Achieve.ACTIVE_IF_EQUALS_TO, 3, "levelStuff"); a.defineProperty ("completedLevels", 0, Achieve.ACTIVE_IF_GREATER_THAN, 5); a.defineProperty ("deaths", 0, Achieve.ACTIVE_IF_EQUALS_TO, 0); a.defineAchievement ("masterKill", ["killedEnemies"]); // Uccidi più di 10 nemici. a.defineAchievement ("cantTouchThis", ["lives"]); // Completa un livello e non morire. a.defineAchievement ("nothingElse", ["completedLevels"]); // Batti tutti e 5 i livelli. a.defineAchievement ("hero", ["completedLevels", "deaths"]); // Batti tutti e 5 i livelli, non morire durante il processo function gameLoop (): void if (enemyWasKilled ()) a.addValue (["killedEnemies"], 1);  if (playerJustDied ()) a.addValue (["lives"], -1); a.addValue (["deaths"], 1);  function levelUp (): void a.addValue (["completedLevels"], 1); a.checkAchievements (); // Ripristina tutte le proprietà con tag "levelStuff" a.resetProperties ("levelStuff"); 

Conclusione

Questo tutorial ha dimostrato un approccio semplice per l'implementazione di risultati nel codice. Concentrandosi su contatori e tag gestiti, l'idea tenta di eliminare numerosi test diffusi in tutto il codice.

Puoi scaricare o biforcare il codice dal suo repository GitHub: https://github.com/Dovyski/Achieve

Spero che questo approccio ti aiuti a realizzare i risultati in un modo più semplice e migliore. Grazie per aver letto! Non dimenticare di tenerti aggiornato seguendoci su Twitter, Facebook o Google+.

Post correlati
  • Falli lavorare per questo: Progetta i risultati per i tuoi giochi
  • Non limitarti a dare una mano: progettare sblocchi per i tuoi giochi