Meine Gedanken zur Spieleprogrammierung - Teil 2 - Spielstandsdaten

D

DocJunioR

Ambitioniertes Mitglied
7
Im letzten Teil haben wir gesehen, wie man einen kleinen Controller bastelt, der die einzelnen Ansichten des Spieles, bzw. Spielmenüs steuert.
Jetzt möchte ich gern zeigen, wie man auf einfache Weise seinen Spielstand vorhalten, laden und speichern kann.

Gehen wir mal davon aus, dass wir ein Spiel haben (wie das dann funktioniert, folgt in einem weiteren Teil) in dem z.B. wir die Informationen Spielername, Level, Punktzahl und Lebensenergie speichern wollen.

Wenn man sich nicht noch über Dateiformate Gedanken machen möchte, gibt es eine sehr einfache Möglichkeit in Java, Daten vorzuhalten: die Klasse Properties.
Sie besteht prinzipiell aus einer Map, die Schlüssel-Wertpaare beinhaltet, hat aber noch eine entscheidene Eigenschaft: Sie kann ihren Inhalt in Text- oder XML-Dateien schreiben und diesen wieder auslesen. Sicherlich wären Spieldaten im Zugriff schneller, wenn wir sie als Klassenhierarchie aufbewahren, aaber dann müssten wir dieses selbst tun.

Wie arbeite ich jetzt aber damit?
Zuallererst einmal müssen meine Spieleviews wissen, dass es einen Spielstand gibt. Dieser wird zentral verwaltet und den Views übergeben. Dazu erweitern wir unser IGameView
Code:
public interface IGameView {

    /**
     * @return game data object
     */
    public Properties getGameData();

    /**
     * @param data game data object
     */
    public void setGameData(Properties data);
    
    /**
     * @param setter Instance of the program setting the active view
     */
    public void setViewSetter (IViewSetter setter);
}

Meine Activity wird Wirt meiner Spieledaten. Dort werden sie instanziiert und beim Erstellen der Views über das Interface übergeben. Dies macht übrigens auch da sinn, wo eigentlich nicht gespielt wird. Beispielsweise braucht ein Menü kein Save anbieten, wenn der Spielstand leer ist.

Code:
public class MainActivity extends Activity implements IViewSetter {
    // Die Spielstandsdaten- Neudeutsch: Geschäftsobjekt
    Properties gameData = new Properties();    

      public void setView(String, viewName) {
       ...
        // mache den ViewSetter bekannt
        view.setViewSetter(this);
        // mache der View den Spielstand bekannt.
        view.setGameData(gameData);
        // setze die neue View
        setContentView ((View) view);
    }

Wieder müssen wir das Interface ständig neu implementieren, bzw. pro Viewtyp vererben. Mein Hauptmenü sieht dann so aus:

Code:
public class MainMenu extends LinearLayout implements IGameView,
    OnClickListener {
    IViewSetter setter = null;

    Properties gameData = new Properties();

    Button startGame = null;
    Button loadGame = null;
    Button saveGame = null;
    Button exit = null;

    private Button createMenuButton(String name) {
    Button b = new Button(getContext());
    b.setText(name);
    b.setVisibility(VISIBLE);
    b.setPadding(2, 2, 2, 2);
    b.getBackground().setAlpha(0x80);
    b.setClickable(true);
    b.setOnClickListener(this);
    this.addView(b);

    return b;
    }

    public MainMenu(Context context) {
    super(context);
    try {
        // set visibility
        this.setVisibility(VISIBLE);
        // gravity is bottom
        this.setOrientation(VERTICAL);
        this
            .setGravity(Gravity.CENTER_VERTICAL
                | Gravity.CENTER_HORIZONTAL);

        startGame = this.createMenuButton("Start Game");
        loadGame = this.createMenuButton("Load Game");

        // wenn Daten im Spieleobjekt enthalten sind,
        // stelle den save-button dar
        if (!this.gameData.isEmpty()) {
        saveGame = this.createMenuButton("Save Game");
        }
        exit = this.createMenuButton("Exit");

    } catch (IOException e) {
        e.printStackTrace();
    }
    }

    @Override
    public Properties getGameData() {
        return gameData;
    }

    @Override
    public void setGameData(Properties data) {
        this.gameData = data;
    }

    @Override
    public void setViewSetter(IViewSetter setter) {
        this.setter = setter;
    }

    @Override
    public void onClick(View v) {
    Log.d("TraderGame", "item click");
    if (v == startGame) {
        this.setter.setView("gameloop");
    } else if (v == loadGame) {
        this.setter.setView("loadgame");
    } else if (v == saveGame) {
        this.setter.setView("savegame");
    } else if (v == exit) {
        this.setter.setView("exit");
    }

    }

}

Als kleines Schmankerl hier noch meine loadgame und savegame-Klassen. Diese sind abgeleitet von einer FileView-Klasse.

Code:
public abstract class FileListView extends LinearLayout implements IGameView,
    OnItemClickListener, OnClickListener {
    protected IViewSetter setter = null;
    protected Properties gameData = new Properties();
    protected File directory = null;

    protected ListView list = null;
    protected EditText filename = null;
    protected Button go = null;

    /**
     * @param context
     */
    public FileListView(Context context) {
    super(context);

    // Savegame-Verzeichnis erstellen
    createDirectory(context);

    // set visibility
    this.setVisibility(VISIBLE);
    // gravity is bottom
    this.setOrientation(VERTICAL);
    this.setGravity(Gravity.TOP | Gravity.LEFT);
    
    LinearLayout view2 = new LinearLayout(context);
    view2.setOrientation(HORIZONTAL);
    view2.setGravity(Gravity.LEFT);
    
    // add filename textfield
    this.filename = new EditText(context);
    this.filename.setWidth(250);
    view2.addView(filename);
    
    // add run button
    this.go = new Button(context);
    this.go.setText("go");
    this.go.setOnClickListener(this);
    view2.addView(go);
    
    this.addView(view2);
    
    // add ListView
    this.list = new ListView(context);
    this.list.setId(R.id.ListView01);
    this.list.setOnItemClickListener(this);
    String fileList[] = directory.list();

    
    this.list.setAdapter(new ArrayAdapter<String>(context, android.R.layout.simple_list_item_1, fileList));
    
    this.addView(this.list);
    }
   

    /**
     * create data directory
     * 
     * @param context
     */
    private void createDirectory(Context context) {
    directory = new File("/sdcard/."
        + context.getResources().getString(R.string.app_name));
    // Erstelle das Unterverzeichnis für die Savegames, falls nötig
    if (!directory.isDirectory()) {
        directory.mkdir();
    }
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.docjunior.gameengine.IGameView#getGameData()
     */
    @Override
    public Properties getGameData() {
    return this.gameData;
    }

    /*
     * (non-Javadoc)
     * 
     * @see de.docjunior.gameengine.IGameView#setGameData(java.util.Properties)
     */
    @Override
    public void setGameData(Properties data) {
    this.gameData = data;

    }

    /*
     * (non-Javadoc)
     * 
     * @see
     * de.docjunior.gameengine.IGameView#setViewSetter(de.docjunior.gameengine
     * .IViewSetter)
     */
    @Override
    public void setViewSetter(IViewSetter setter) {
    this.setter = setter;
    }

    @Override
    public void onItemClick(AdapterView<?> arg0, View arg1, int item, long arg3) {
    this.filename.setText(this.list.getItemAtPosition(item).toString());
    }

    /**
     * gets the created file
     * @return
     */
    protected File getFile() {
    File f = null;
    if (!this.filename.getText().equals("")) {
        String dir = this.directory.getAbsolutePath();
            dir += "/" + this.filename.getText();
            dir.replace(" ", "_");
        Log.d(getContext().getString(R.string.app_name), "File:" + dir );    
        f = new File (dir);    
    }
    return f;
    }
}

Mein LoadGame sieht dann ca. so aus:
Code:
public class LoadGame extends FileListView {

    /**
     * @param context
     */
    public LoadGame(Context context) {
    super(context);
    // TODO Auto-generated constructor stub
    }

    /* (non-Javadoc)
     * @see android.view.View.OnClickListener#onClick(android.view.View)
     */
    @Override
    public void onClick(View v) {
    if (v == this.go) {
        InputStream is;
        try {
        is = new FileInputStream(getFile());
        this.gameData.load(is);
        this.setter.setView("gameloop");
        } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        }
    }

    }

}

und das SaveGame
Code:
public class SaveGame extends FileListView {

    /**
     * @param context
     */
    public SaveGame(Context context) {
    super(context);
    // TODO Auto-generated constructor stub
    }

    /*
     * (non-Javadoc)
     * 
     * @see android.view.View.OnClickListener#onClick(android.view.View)
     */
    @Override
    public void onClick(View v) {
    if (v == this.go) {
        OutputStream os;
        try {
        os = new FileOutputStream(getFile());
        this.gameData.store(os, "save game at" + new Date());
        this.setter.setView("mainmenu");
        } catch (FileNotFoundException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        }
    }
    }

}

Soo, jetzt haben wir eigentlich alles, was wir benötigen, um eine Spiele-View zu schreiben. Nur, hat diese am Anfang gar keine Daten. Ich habs mir da einfach gemacht. Wenn mein GameView am Anfang keine Datem jat, die gameDate.isEmpty() also true liefern, wird einfach ein initialer Spielstand aus den assets geladen:

Code:
    public void setGameData(Properties gameData) {
    // TODO Auto-generated method stub
    this.gameData = gameData;
    if (gameData.isEmpty()) {
        InputStream in;
        try {
        in = getContext().getAssets().open("newgame.properties");
        gameData.load(in);
        } catch (IOException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
        }
    }
    }

Der nächste Teil zeigt dann, wie man eine View mit GameLoop baut, um z.b. KI-Spieler für sich arbeiten zu lassen..

Ich hoff, ich hab euch ein wenig was Interessantes erzählen können. Kritik und Anregungen sind wie immer erwünscht.
 
DocJunioR schrieb:
Wenn man sich nicht noch über Dateiformate Gedanken machen möchte, gibt es eine sehr einfache Möglichkeit in Java, Daten vorzuhalten: die Klasse Properties.
Sie besteht prinzipiell aus einer Map, die Schlüssel-Wertpaare beinhaltet, hat aber noch eine entscheidene Eigenschaft: Sie kann ihren Inhalt in Text- oder XML-Dateien schreiben und diesen wieder auslesen. Sicherlich wären Spieldaten im Zugriff schneller, wenn wir sie als Klassenhierarchie aufbewahren, aaber dann müssten wir dieses selbst tun.

Kleine Anmerkung:
Für Android gibt's auch die Klasse Preference: Preference | Android Developers Sie speichert ihre Daten in einer sqlite Datenbank. Vorteil ist, dass man von überall aus zugreifen kann, ohne sich Gedanken über Dateienamen oder Speicherorte machen zu müssen. Vermutlich ist sie auch ein wenig schneller, da nicht jedesmal ein XML file geladen und geparst werden muss.

Man holt sich einfach eine SharedPreferences editor Instanz und kann dann ganz bequem alles reinschreiben.
 
ja, aaaber dann kannst du meines Wissens nur einen Spielstand laden. Es muss ja auch nicht xml sein. So, wie's im Code steht, speichert er einfache Schlüssel = Wert - Paar in Textdateien.
Ansonsten ists aber auch Auffassungsfrage ;) hab ja gesagt, bin nicht der Weisheit letzter Schluss.

Edit: Hab mir gerade die Preferences genauer angeschaut. Sie arbeiten auch über einen XML-Baum, sind aber im Gegensatz zu Properties hierarchisch aufbaubar. Sogar im- und exportieren kann man. Bei komplexeren Daten sicher garnicht so schlecht.
Ich kenn halt die Android-API noch nicht soo gut und beschreibe eben auch nur meine Gedankengänge, als JavaSEler
 
Zuletzt bearbeitet:
kleiner tipp: von dateien ist generell abzuraten. ( in sehr seltenen fällen mögen sie ja sinnvoll sein, aber ganz ganz ganz selten )

es gibt diese shared preferences und die sqlite db. die datenbank liegt ja direkt am händy, wodurch der zugriff ja extrem schnell ist ( im vergleich natürlich )
 
naaja, ich weiß nicht, ob es sich wirklich lohnt, für sowas ne datenbank zu entwickeln und eigenlich wird die datei ja nur zum lade- und speicherzeitpunkt angefasst, der rest läuft im ram ab . insofern ist performance eher unkritisch.
prinzipiell ist es aber egal, wie persistiert wird. der transport des geschäftsobjekts- wie auch immer dieses aussieht- bleibt gleich ;)
Ich persönlich bleibe vorerst bei dateien.
 
ja war auch nur ein hinweis, wie man es in der android welt eher bevorzugen sollte

db entwickeln ist dafür schon ein wenig hoch gegriffen;) einfach eine tabelle mit paar werten und passt

aber viele wege führen nach rom , das ist klar
 
eeben.und ich bin der letzte, der irgendwas dogmatisiert
 

Ähnliche Themen

Temucin
Antworten
1
Aufrufe
533
jogimuc
J
D
Antworten
3
Aufrufe
447
jogimuc
J
P
Antworten
9
Aufrufe
1.933
pseudopat
P
Zurück
Oben Unten