Animationen mit Hilfe von After Effects erstellen


Im vorherigen Blogeintrag wurde bereits beleuchtet, wie man animierte Texturen für die Vektoria-Engine aufbereitet. Als Basis für das radial verschwindende Animationsicon diente eine PNG-Bildsequenz. In diesem Blogeintrag soll es nun darum gehen, wie man zu einer solchen Sequenz kommt. Ziel ist dabei nur den grundsätzlichen Ablauf zu erläutern, welche bei allen Animationen in etwa gleich sein wird.

Erstellt wird die Sequenz mit Hilfe von Adobe After Effects, einem Animations- und Compositing-Programm. Für das Icon aus dem letzten Blogeintrag gab es bereits eine PSD-Datei, welche dann in After Effects geladen und dort animiert wurde. Hier möchte ich jedoch ein neues Beispiel bringen. Konkret geht es um eine horizontale Blitzanimation. Ein einzelnes Bild könnte dabei wie folgt aussehen:

Animierter Blitz

Für die Animation (im Gegensatz zur letzten) wird auch kein Ausgangsmaterial benötigt. Sie wird rein mit Bordmitteln aus After Effects erstellt.

Zuerst muss eine neue Komposition erstellt werden (Komposition -> Neue Komposition...). Für den Blitz habe ich eine Größe von 1024 px ∗ 1024 px gewählt (die Endgröße lässt sich am Ende immer noch anpassen) und das Pixel-Seitenverhältnis auf quadratisch gestellt. Dann noch die Framerate und Zeit so einstellen, dass am Ende so viele Bilder ausgegeben werden, wie gewünscht. Das Ergebnis könnte dann folgendermaßen aussehen:

Kompositionseinstellungen für den animierten Blitz

Für die Blitzanimation reicht ein einfacher Effekt. Um ihn anwenden zu können, muss jedoch erst eine neue Farbfläche (Farbe spielt keine Rolle) der Komposition hinzugefügt werden (Ebene -> Neu -> Farbfläche...). Nun muss der Effekt angewandt werden. Dazu im Fenster "Effekte und Vorgaben" nach "Blitz - Horizontal" suchen und den Effekt auf die Farbfläche anwenden. In den Effekteinstellungen den Blitz noch wie gewünscht anpassen (oft empfiehlt es sich, einfach etwas herumzuprobieren).

Damit der Blitz noch etwas Bewegung bekommt, muss er noch animiert werden. Es können alle Effekteinstellungen animiert werden, welche mit einer Stoppuhr versehen sind. Ich habe mich hier für die Einstellung "Leitungszustand" entschieden. Für die Animation am Anfang (Slider ganz links) auf die Stoppuhr drücken und einen Wert (z. B. 0.0) einstellen. Danach an das Ende springen (Slider ganz rechts) und den Zielwert (z. B. 10.0) bestimmen. Durch letztere Aktion wird automatisch ein neuer Keyframe gesetzt und zwischen den beiden Keyframes wird (linear) interpoliert.

Damit ist die Animation auch bereits abgeschlossen. Jetzt muss das ganze noch als PNG-Sequenz exportiert werden. Dazu den Befehl Komposition -> An die Renderliste anfügen auswählen. Folgende Bereiche sind nun relevant:

  • Rendereinstellungen: Hier kann noch im Nachhinein die Auflösung verändert werden, um z. B. die einzelnen Bilder nur in einer Auflösung von 256 px ∗ 256 px zu rendern. Das wird wichtig, wenn die Animationen zu groß werden.
  • Ausgabemodul: Hier sollte als Format "PNG Sequenz" und bei den Kanälen "RGB + Alphakanal" stehen.
  • Speichern unter: Neben dem Dateinamen kann hier die Anzahl der Stellen für die Nummerierung der Ausgangsbilder angegeben werden. Im Dateinamen tauchen dazu mehrere "#" auf, welche für je eine Ziffer stehen. Achtet darauf, dass ihr die Anzahl der Stellen minimal haltet, d. h. bei 30 Bildern dürfen auch nur 2 Ziffern im Dateinamen stehen.

Wenn alle Einstellungen gemacht wurden, einfach auf "Rendern" klicken und schon wird die Animation gerendert und als PNG-Sequenz abgespeichert. Diese kann nun mit dem Tool aus dem vorherigen Blogeintrag zu einem einzigen Bild zusammengefügt und dann in der Vektoria-Engine verwendet werden.

Texturen in Photoshop per Skript automatisiert auf unterschiedlichen Systemen erzeugen


Jedes Spiel benötigt Texturen und so auch wir in unserem DV-Projekt. Dabei kann es immer wieder Anwendungsfälle geben, bei denen bestimmte Texturen automatisch generiert werden sollen. Dieser Blogeintrag soll nun zeigen, wie das mit Hilfe von Adobe Photoshop funktioniert, so dass es auch auf unterschiedlichen Systemen läuft.

In unserem Projekt haben wir eine Spielkarte und dort für die einzelnen Felder verschiedene Symbole. Die Symbole haben durchaus gewisse Gemeinsamkeiten, wie einen gemeinsamen Hintergrund. Daher sind alle Symbole in einer PSD Datei abgelegt. Von dort aus können dann die verschiedenen Texturen für die verschiedenen Felder erzeugt werden. Im Folgenden sind zwei Beispieltexturen für ein Windkraftwerk und ein Atomkraftwerk zu sehen, welche sich beide den gleichen Hintergrund teilen.

Feld für ein Windkraftwerk Feld für ein Atomkraftwerk

Wird nun beispielsweise der Hintergrund geändert, so müssen alle Texturen für alle Felder erneut erzeugt und abgespeichert werden. Insbesondere da auch andere Texturen wie Bump-Maps auch noch generiert werden müssen, kann sich bereits einiges ansammeln. Eine manuelle Bearbeitung gestaltet sich daher sehr schnell als mühselig.

In Photoshop bietet es sich daher an mit Aktionen zu arbeiten. Das sind Makros, mit denen man die durchgeführten Schritte in Photoshop aufzeichnen kann. Einmal erstellt, können sie dann einfach per Tastendruck ausgeführt werden. Das ist also genau das, was sich hier anbietet. Die Speicherung sämtlicher Feldtexturen kann so nun automatisiert angesteuert werden. Mit einer Aktion werden bei uns beispielsweise 18 Texturen erzeugt. Diese jedes Mal manuell zu erzeugen, wäre eindeutig zu viel Arbeit. Ein kleiner Auszug aus den Aktionsschritten ist im Folgenden Bild zu sehen.

Aktion in Photoshop

Das ist auf jeden Fall schon einmal ein Schritt in die richtige Richtung. Leider ist es nun so, dass Photoshop bei Speicheraktionen immer den absoluten Pfad abspeichert. Damit kann die Aktion leider nicht mehr bei anderen Nutzern ausgeführt werden (da die Pfade nicht passen). Es gibt leider auch keine Einstellungsmöglichkeit, welche diesen Umstand beheben würde.

Es gibt aber eine andere Lösung. Neben den Aktionen gibt es in Photoshop auch die Möglichkeit Scripte in der Programmiersprache Javascript zu schreiben, welche ebenfalls Abläufe in Photoshop automatisieren können. Natürlich wäre es etwas mühsam, wenn man nun alle Schritte erneut in ein Skript übernehmen müsste. Zum Glück besteht jedoch die Möglichkeit, dass man eine bestehende Aktion in ein Skript umwandeln kann. Dieser Schritt wird selbst wiederum von einem Script übernommen, welches hier heruntergeladen werden kann.

Dazu muss man nur vorher seine Aktion als *.atn Datei abspeichern, dann das heruntergeladene Script ausführen (Datei -> Skripten -> Durchsuchen...) und diese eben erzeugte *.atn Datei mitgeben. Erzeugt wird daraus eine entsprechende *.js Datei, welche die gleichen Schritte wie die Aktion enthält.

Noch stehen dort jedoch ebenfalls die absoluten Pfade. Daher muss man nun die erzeugte *.js Datei öffnen und alle absoluten Pfadangaben durch relative Pfade ersetzen. Im Folgenden ist ein kleiner Ausschnitt aus unserem Script zu sehen, bei welchen die Pfade angepasst wurden.


...
"'In  '":
{
	"<path>": ".\\texture_terrain_base_diffuse.png"   // Relative Pfadangabe
},
"'DocI'":
{
	"<integer>": 35
},
...

Das Script dann am besten zusammen mit der *.psd Datei abspeichern. Jetzt kann jeder Nutzer das Script bei sich ausführen und Dank der relativen Pfade sollte das dann auch problemlos funktionieren.

Statische Klassen als Hilfsmittel zur zentralen Datenspeicherung


Bei der Entwicklung eines Spieles gibt es viele verschiedene Daten zu speichern, welche zwar eigentlich zu bestimmten Objekten gehören, auf der anderen Seite aber auch eine zentrale Speicherung wünschenswert wäre. In unserem Projekt sind dies beispielsweise Materialien, Balanceinformationen oder auch Sounds. Wir haben festgestellt, dass es für diese Art von Daten praktisch ist, wenn diese zentral an einer einzigen Stelle abgespeichert werden. Daher soll es in diesem Blogeintrag um ein einheitliches Konzept gehen, welches wir verwendet haben, um diese Art von Daten zu speichern.

Eine zentrale Speicherung (im Gegensatz zu einer verteilten in X Klassen) hat den entscheidenden Vorteil, dass man sofort weiß, wo man nachsehen muss, falls man einen Datensatz ändern möchte. Will man beispielsweise ein bestimmtes Material austauschen, so gibt es bei uns nur eine Datei, welche dafür angepasst werden muss. Ein weiterer Vorteil ist die einfachere Zusammenarbeit im Team. Wenn es für bestimmte Daten (z. B. Materialien) nur eine zentrale Anlaufstelle für die Speicherung gibt, weiß jedes Teammitglied auch sofort, wo es suchen muss. Gerade im Fall von Materialien können in unserem Projekt die Modellierer ihre Modelle mit Testtexturen versehen und Einträge für ihre Materialien hinterlegen. Darauf aufbauend können die Texturierer die Materialien durch die richtigen ersetzen. Und das alles über eine zentrale Anlaufstelle.

Neben den bereits angesprochenen Materialien verwenden wir diese Technik zur Zeit auch noch für Balanceinformationen und für Sounds. Unter dem Balancing sind vor allem Daten relevant, die eine starke Auswirkung auf den Spielfluss haben (und natürlich auch sehr stark vom jeweiligen Spiel abhängen). Bei uns ist dies beispielsweise das Bevölkerungswachstum der Stadt in Einwohner pro Sekunde. Die zentrale Stelle für Sounds sammelt bei uns alle Soundeffekte und die verwendete Musik.

Die technische Realisierung läuft bei uns über statische Klassen ab. Das hat den großen Vorteil, dass von überall heraus sehr einfach auf die Daten dieser Klasse zugegriffen werden kann. Werden die Daten dagegen nur in einem bestimmten Objekt abgespeichert, muss man immer dafür sorgen, dass eine entsprechende Referenz auf dieses Objekt auch in allen Klassen zur Verfügung steht. Das ist etwas, was mit der Zeit sehr lästig werden kann. Zusätzlich ist die Verwendung einfacher. Man benötigt nur den Klassennamen und die entsprechende Methode, um z. B. einen Soundeffekt abzuspielen. Wenn man mit Objekten arbeitet, besteht zudem gerade in C++ immer die Gefahr, dass durch falsche Verwendungen bei der Parameterübergabe (ungewollte) Kopien von Objekten erzeugt werden.

Im Folgenden ist die Grundstruktur für eine solche statische Klasse am Beispiel unseres Soundloaders gegeben (Headerdatei, kompletter Inhalt kann auch auf Github eingesehen werden):


class VSoundLoader
{
private:
	VSoundLoader() = delete;
	VSoundLoader(const VSoundLoader&) = delete;
	VSoundLoader(const VSoundLoader&&) = delete;
	VSoundLoader& operator=(const VSoundLoader&) = delete;
	VSoundLoader& operator=(const VSoundLoader&&) = delete;
	~VSoundLoader() = delete;

private:
	DEBUG_EXPRESSION(static bool initDone);
	// More variable declarations...

public:
	static void init(/** Some parameter **/);
	static void playBackgroundMusicIngame();
};

Als erstes wird unterbunden, dass ein Objekt dieser Klasse erzeugt werden kann (schließlich soll die Klasse ja komplett statisch sein). Dazu werden sämtliche Implementierungen, welche der Compiler normalerweise automatisch erzeugt, abgeschaltet. Es würde reichen, nur den Konstruktor zu verbieten, aber der Vollständigkeit halber wurde hier alles ausgeschlossen.
Als nächstes gibt es eine Variable, welche anzeigt, ob die statische Klasse initialisiert wurde. Dies ist praktisch als Fehlerüberprüfung (im Debug Modus), wie in der Implementierungsdatei noch zu sehen.
Dann kommen noch die Methoden, welche auf dieser Klasse ausgeführt werden können. Bei allen unseren Klassen gibt es immer eine init()-Methode, welche die Initialisierung der einzelnen Variablen übernimmt. In diesem Fall können dort die einzelnen Sounddateien eingelesen werden. Es muss dafür gesorgt werden, dass diese Methode einmal während des Programmstartes (möglichst am Anfang) aufgerufen wird. Danach kann die Klasse frei verwendet werden.

Abschließend noch ein kleiner Blick in die zugehörige Implementierungsdatei:


DEBUG_EXPRESSION(bool VSoundLoader::initDone = false);
DEBUG_EXPRESSION(static const char* const assertMsg = "SoundLoader is not initialized");
//More variable definitions...

void VSoundLoader::init(/** Some parameter **/)
{
	// Some init stuff

	DEBUG_EXPRESSION(initDone = true);
}

void VSoundLoader::playBackgroundMusicIngame()
{
	ASSERT(initDone, assertMsg);

	// Some magic
}

Hier wird jetzt auch klar, warum es eine Variable gibt, welche den Initialisierungsstatus überprüft. Am Ende der init()-Methode wird diese Variable auf true gesetzt. Bei allen anderen Methoden wird nun zuerst überprüft, ob die Initialisierung auch wirklich durchgeführt wurde. Dadurch wird die korrekte Verwendung dieser Klasse sichergestellt. Wenn man jetzt von irgendwo im Programmcode die Hintergrundmusik abspielen möchte, braucht man nur VSoundLoader::playBackgroundMusicIngame() aufzurufen und schon gibt es musikalische Untermalung.

Wie bereits erwähnt, haben wir bis jetzt insgesamt drei solcher Klassen und soweit hat sich dieses Vorgehen auch als eine gute Idee herausgestellt.

Kompilierungszeit mit Hilfe eines Multicorebuild verringern


In einem vorhergehenden Blogeintrag habe ich bereits über die Möglichkeiten einer PCH Datei gesprochen, um die Kompilierungszeit zu verringern. In diesem Blogeintrag möchte ich nun noch eine weitere Möglichkeiten vorstellen, welche die Kompilierungszeit verringern kann.

Normalerweise wird für die Kompilierung immer nur ein Prozessorkern verwendet. Da aber heutige Systeme meist mehrere CPU-Kerne haben, wird das System so nicht voll ausgelastet. In Visual Studio gibt es daher die Möglichkeit den Buildvorgang auch mit mehreren Prozessorkernen zu starten. Die entsprechende Compileroption heißt /MP (Build with Multiple Processes) [Eigenschaften -> Allgemein -> Komplilierung mit mehreren Prozessoren]. Standardmäßig ist sie deaktiviert.

Das klingt jetzt erst einmal nach einer tollen Funktion, welche doch eigentlich immer aktiviert sein sollte. Leider gibt es bei dieser Option auch eine Schattenseite. Eine Kompilierung mit mehreren Prozessorkernen bedeutet ein paralleles bearbeiten der einzelnen Dateien. Wenn man es mit Parallelprogrammierung zu tun hat, muss man immer auch mit den entsprechenden Nachteilen kämpfen (Synchronization, Race Condition, etc.). Während der Kompilierungsphase werden vom Compiler die Ergebnisse in mehreren speziellen Dateien geschrieben. Greifen nun mehrere Threads gleichzeitig auf diese Dateien zu, kann die Konsistenz verloren gehen. Aus diesem Grund gibt es leider eine ganze Reihe von Optionen, welche mit der /MD Option inkompatibel sind.

Eine davon ist für die Kompilierung besonders ärgerlich: /Gm (Enable Minimal Rebuild) [Eigenschaften -> C/C++ -> Codegenerierung -> Minimale Neuerstellung aktivieren]. Bei dieser Option notiert sich der Compiler in speziellen Dateien die Abhängigkeiten zwischen den Sourcedateien und den jeweiligen Klasseninformationen (welche sich normalerweise in den Headerdateien befinden). Anhand dieser Abhängigkeitsinformationen kann der Compiler bei einer entsprechenden Änderung der Headerdatei herausfinden, ob die Sourcedatei (welche die Headerdatei einbindet) neu kompiliert werden muss. Im normalen Programmierablauf kann man sich so unter gewissen Bedingungen die Kompilierung einiger Sourcedateien sparen. Für einen kompletten Rebuild bringt diese Information jedoch nichts (ich verstehe allerdings nicht, warum bei einer kompletten Neuerstellung nicht standardmäßig die /MD Option aktiviert werden kann).

Im Gesamten heißt das also, man hat entweder die Wahl zwischen der Kompilierung mit mehreren Prozessorkernen oder der Kompilierung, welche bei Änderungen nur so wenig Dateien wie möglich neu kompiliert. Was besser ist kann nicht verallgemeinert werden und kommt stark auf den Anwendungsfall an. Insbesondere das jeweilige System und hier die Anzahl der zur Verfügung stehenden Prozessorkernen spielen eine große Rolle. Für unser Projekt habe ich mich dazu entschlossen, diese Option zu aktivieren, da sie zumindest auf meinem System dazu führt, dass der komplette Rebuild in weniger als 20 Sekunden abgeschlossen werden kann. Interessanterweise muss dieser relativ häufig durchgeführt werden (z. B. nach einem Branchwechsel).

Um die /MD Option für ein bestimmtes Projekt zu aktiveren, verwendet man am besten ein Projekteigenschaftenblatt. Ich habe dies bei unserem Projekt so gemacht und das entsprechende Blatt diesem Blogeintrag angehängt. Dabei wird die /MD Option aktiviert und die /Gm Option deaktiviert.

KompilierungszeitMulticorebuild_MulticoreBuild.props

Boost Graphen mit Hilfe von Graphviz anzeigen


In unserem DV-Projekt verwenden wir einen Graphen für die interne Darstellung des Spielfeldes. Damit prüfen wir, ob ein Kraftwerk mit der Stadt über Stromtrassen verbunden ist. Dabei ist jedes Feld ein Knoten im Graph und die Kanten entsprechen den Verbindungen, welche über die Trassen hergestellt werden. Bei einer Spielfeldgröße von 20 x 20 Feldern besitzt auch der entsprechende Graph bereits eine beachtliche Größe.

Insbesondere für Debugging-Zwecke ist es praktisch, wenn man sich den Graphen auch darstellen lassen kann. Es gibt von Boost bereits vorgefertigte Methoden, welche den Graphen auf der Konsole ausgeben lassen, das wird aber bei größeren Graphen sehr schnell unübersichtlich. Zum Glück gibt es bei Boost aber auch die Möglichkeit den Graphen in der DOT-Sprache auszugeben.

Dabei können beispielsweise Knoten, Knotennamen, Kanten und Kantennamen ausgegeben werden. In unserem Fall sind für das Spielfeld nur die Knoten, Kanten und Knotennamen relevant. Für die Knotennamen wäre es wünschenswert, wenn im Namen die Indizierung auftauchen würde. Wir greifen auf die Knoten per Zeilen- und Spaltenindex zu, daher sollten diese auch im Knotennamen auftauchen.

Realisiert werden kann das wie im folgenden Beispiel zu sehen. Für die Namensgebung (Indizes) wird ein std::vector von std::string angelegt. Dort wird für jeden Eintrag im Graphen als Name das Paar (Zeilenindex, Spaltenindex) hinterlegt. Die eigentliche Arbeit verrichtet die Methode write_graphviz, welche einen Ausgabestream zu einem File, den Graphen und die gerade eben generierten Kennzeichnungen entgegen nimmt.


#include <boost/graph/graphviz.hpp>
using namespace boost;
...
std::vector<std::string> names(fieldLength*fieldLength);

for (int x = 0; x < fieldLength; x++) {
	for (int y = 0; y < fieldLength; y++) {
		names[convertIndex(x, y)] = names[convertIndex(x, y)] = std::to_string(x) + std::string(", ") + std::to_string(y);
	}
}

std::ofstream file;
file.open("graph.dot");
if (file.is_open()) {
	write_graphviz(file, powerLineGraph, make_label_writer(&names[0]));
}

Führt man diesen Code aus, so erhält man eine entsprechende graph.dot Datei. Um deren Inhalt nun anzuzeigen, wird noch Graphviz benötigt. Dort ist das Programm dot enthalten, welches die Datei lesen und daraus einen Graphen plotten kann. In unserem Fall ist der Graph sehr groß und gerade am Anfang haben viele Knoten noch gar keine Kanten. Daher wird vorher noch eine Filterung mit dem Programm gvpr durchgeführt, um alle Knoten ohne Kanten aus dem Graphen zu löschen. Eine entsprechende Batch-Datei könnte dabei wie folgt aussehen (in der ersten Zeile wird nur der Pfad zum graphviz Verzeichnis gesetzt):


set PATH=<YOUR_GRAPHVIZ_PATH>\bin;%PATH%
gvpr -c "N[$.degree==0]{delete(0,$);}" graph.dot | dot -Tpng > graph.png

Als Ergebnis erhält man dann beispielsweise folgenden Graphen:

Erzeugte Graphen

Kompilierungszeit mit Hilfe eines precompiled header (PCH) deutlich verringern


Wenn Projekte in C++ etwas größer werden, kommen relativ schnell unangenehme Eigenschaften zum tragen. Eine davon ist die immer mehr ansteigende Kompilierungszeit. Nach nur ein paar grundlegenden Klassen hatten wir in unserem Projekt auf meinem System mit Visual Studio 2013 für einen kompletten Rebuild 105 Sekunden zu warten. Das ist auf Dauer natürlich viel zu lange, daher musste eine Lösung her. Durch mehrere Verbesserungen konnte dieser Wert auf 15 Sekunden reduziert werden. Ein Teil davon wurde durch die Verwendung einer PCH (siehe unten) erreicht und diesen möchte ich hier genauer beschreiben.

Bevor ich auf die konkreten Schritte eingehe, verliere ich zuerst ein paar grundlegende Worte über den Kompilierungsvorgang. Dieser stellt sich nämlich nicht als sonderlich intelligent heraus.

Hintergründe zum allgemeinen Kompilierungsvorgang

Für den Kompilierungsvorgang wird jede .cpp Datei als eigenständige Kompilierungseinheit betrachtet. Für einen kompletten Rebuild müssen also alle .cpp Dateien gelesen und kompiliert werden. Werden in den .cpp Dateien nun Headerdateien eingebunden, so müssen auch diese eingelesen und kompiliert werden. Das Problem ist nun, dass diese Einheiten unabhängig voneinander agieren. Werden die gleichen Headerdateien in verschiedenen .cpp Dateien eingelesen, bedeutet das, dass diese für jede .cpp Datei einzeln eingelesen und kompiliert werden müssen. Bemerkbar macht sich dies insbesondere bei großen Headerdateien wie beispielsweise Header aus der Standardbibliothek.

Als Beispiel sei folgendes Bild gegeben:

Beispielhafte Include-Hierarchie

Wir haben es mit zwei Implementierungsdateien zu tun (A.cpp und B.cpp). A.cpp inkludiert A.h und common.h. B.cpp inkludiert B.h und ebenfalls die common.h. Da der Kompilierungsvorgang getrennt abläuft, wird die common.h zweimal eingelesen und kompiliert. Wenn die common.h nun folgendermaßen aussehe


#pragma once

#include <vector>
#include <map>
#include <list>
#include <string>
#include <sstream>
#include <memory>

kann man sich leicht vorstellen, warum die Kompilierungszeiten so schnell ansteigen können (die Headerdateien aus der Standardbibliothek können verdammt groß werden).

Zum Glück gibt es für dieses Problem eine Lösung und die heißt precompiled header (PCH). Damit wird der Compiler veranlasst, bestimmte Headerdateien nur einmal zu kompilieren und das Ergebnis dann für alle Kompilierungseinheiten (.cpp Dateien) wiederzuverwenden. Damit löst man genau das beschriebene Problem. Wäre die common.h im PCH, kompiliere sie im Beispiel nur einmal anstatt zweimal.

Die PCH ist letztendlich selbst nur eine (besondere) Headerdatei. In Visual Studio wird sie meisten bei neuen Projekten mit angelegt und heißt stdafx.h. Dort können nun alle Headerdateien eingetragen werden, welche für den kompletten Kompilierungsvorgang nur einmalig kompiliert werden sollen. Damit das ganze allerdings funktioniert muss diese Headerdatei in allen anderen Dateien (sowohl .h als auch in .cpp Dateien) als allererstes eingebunden werden. Zum Glück gibt es aber auch dafür eine automatisierbare Lösung.

Bevor ich auf die Schritte eingehe, welche für Visual Studio notwendig sind, vorweg noch eine Warnung. Dadurch, dass die PCH in allen anderen Dateien eingebunden werden muss, bedeutet eine Änderung der PCH immer einen kompletten Rebuild. Eine Änderung tritt auch auf, wenn eine der Headerdateien, welche die PCH einbindet, verändert wird. Aus diesem Grund sollten dort nur Headerdateien stehen, welche sich nur sehr selten ändern. Die Headerdateien der Standardbibliothek sind dafür ein gutes Beispiel.

Einrichtung in Visual Studio 2013

  1. Zuallererst müsst ihr dafür sorgen, dass es die entsprechenden stdafx.h und stdafx.cpp Dateien gibt. Meistens sind diese bereits vorhanden. Falls nicht, kann man sie ganz normal manuell anlegen.
  2. Das Projekt muss für die PCH Datei eingerichtet werden. Dazu unter Projekt -> Eigenschaften -> C/C++ -> Vorkompilierte Header wechseln. Unter "Vorkompilierter Header" sollte Verwenden (/Yu) und unter "Vorkompilierte Headerdatei" stdafx.h stehen.
  3. Es muss noch dafür gesorgt werden, dass die PCH in jeder Datei des Projektes als erstes eingebunden wird. Dazu auf den Reiter "Erweitert" wechseln und dort unter "Erzwungene Includedateien" stdafx.h eintragen. Anschließend die Projekteigenschaften wieder schließen.
  4. Nun muss man Visual Studio noch mitteilen, dass die entsprechende PCH erstellt werden soll. Dazu die Eigenschaften der Datei stdafx.cpp öffnen (Rechtsklick -> Eigenschaften) und unter C/C++ -> Vorkompilierte Header bei "Vorkompilierte Header" Erstellen (/Yc) auswählen.

Die PCH könnte dabei wie die folgende aussehen. Bis auf die letzte Zeile handelt es sich dabei nur um automatisch generierte Einträge.


// stdafx.h : Includedatei für Standardsystem-Includedateien
// oder häufig verwendete projektspezifische Includedateien,
// die nur in unregelmäßigen Abständen geändert werden.
//

#pragma once

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>

// Hier auf zusätzliche Header, die das Programm erfordert, verweisen.
#include "common.h"

Es wurde dabei nur die common.h und nicht die einzelnen Headerdateien selbst innerhalb der common.h eingebunden. Das hat den Grund, dass das Projekt weiterhin kompilierbar sein sollte, auch wenn die PCH nicht eingerichtet ist. Das heißt, ein Projekt sollte nicht die Verwendung einer PCH voraussetzten, damit es sich kompilieren lässt (Eine Mehrfachinkludierung wird durch die entsprechenden include guards sichergestellt.

Das waren alle notwendigen Schritte. Nun könnt ihr das Projekt neu kompilieren. Wenn alles funktioniert hat, wird zuerst die stdafx.cpp und dann die restlichen Dateien kompiliert.

Da die Vekroria-Engine in ihren Headerdateien aktuell noch regen Gebrauch von der Anweisung using namespace std; macht, können diese Headerdateien noch nicht in die PCH eingebunden werden (obwohl sie sich eignen würden). Durch die genannte Anweisung kommt es leider (zumindest bei unserem Projekt) zu Namenskonflikten. Wenn spätere Versionen der Vektoria-Engine diese Anweisung jedoch nicht mehr enthalten, sollten auch diese Headerdateien in der PCH eingebunden werden können.

Statische Codeanalyse in Visual Studio mit Cppcheck


Bei der Entwicklung mit C++ gibt es viele kleine Fallen, in die man tappen kann, welche teilweise mit großen Auswirkungen verbunden sind. Ein beliebtes Beispiel für eine solche Falle ist das einfache Vergessen einer Variableninitialisierung. Im folgenden Beispiel hält die Klasse ClassA einen Pointer auf die Klasse ClassB, welcher aber nie initialisiert wird. Eine Verwendung dieses Pointers in irgendeiner Methode der Klasse ClassA führt im besten Fall zu einem Speicherlesefehler, im schlechtesten Fall zeigt der Pointer auf andere Teile des Programms, was schnell zu schwierig reproduzierbaren Bugs inklusive Datenverlust führen kann.


class ClassB;

class ClassA
{
private:
	ClassB* ptr;

public:
	ClassA()
	{}
	//...
};

Im Falle eines Pointers ist das Problem sogar noch gravierender. Da in C++ die Variablen vom Compiler nicht initialisiert werden, besitzt der Pointer ptr dadurch irgendeinen Wert, welcher zufällig an dieser Speicherstelle zu finden war. Das ist insbesondere deswegen gravierend, da so sämtliche if(ptr == nullptr) {...}-Prüfungen ebenfalls fehlschlagen (da die Variable eben nicht initialisiert wurde).

Der Compiler kompiliert diesen Code anstandslos und gibt auch keine Warnung aus, da der entsprechende Standard hier auch keine Vorschriften macht. Zum Glück sind derartige Probleme aber so geläufig, dass sich statische Codeanalysetools etabliert haben, welche die Quellcodedateien analysieren und nach entsprechenden Fehlern absuchen. Das hier beschriebene Problem ist dabei nur beispielhaft ausgewählt. Es gibt noch viel mehr solcher Fälle und daher finden solche statischen Analysetools auch mehr als nur das hier genannte Problem.

Ein solches Tool ist Cppcheck. Es ist frei, Open Source und auch als portable Version erhältlich. Insbesondere für unser Projekt interessant ist, dass es auch für Visual Studio eine entsprechende Extension gibt, welche die direkte Einbindung in die Entwicklungsumgebung ermöglicht. So können Dateien beispielsweise nach jedem Speichern automatisch einer Analyse unterzogen werden. Für das Beispiel sieht dies folgendermaßen aus:

Cppcheck-Fenster in Visual Studio

Was man dafür tun muss:

  1. Cppcheck und die Visual Studio Extension herunterladen
  2. Installieren
    • Bei erster Verwendung aus Visual Studio heraus den vollständigen Pfad zur cppcheck.exe angeben.
  3. Geöffnetes und ausgewähltes Projekt in Visual Studio analysieren: Extras → Tools → Check current project with cppcheck

Möchte man keine ständige Prüfung der Dateien nach dem Speichern, so kann man dies unter Cppcheck settings entsprechend deaktivieren.

Die Verwendung eines statischen Analysetools ist auf jeden Fall empfehlenswert, da so leicht Fehler vermieden werden können, welche ansonsten vielleicht nur sehr schwer zu finden und zu debuggen wären.

Über öffentliche Termine im Google Kalender benachrichtigt werden


Da der Wunsch über eine Benachrichtigung für neue Termine für unseren öffentlichen Kalender DV-Projekt aufkam, hier nun eine kurze Anleitung, wie ihr euch entsprechend (per E-Mail) informieren lassen könnt.

  1. Im Kalender über den entsprechenden Werkzeug-Button auf die Einstellungen wechseln.
  2. Dort auf den Reiter Kalender wechseln.
  3. Weiter unten sollte der öffentliche Kalender DV-Projekt auftauchen: Google-Kalender Benachrichtigungen 1
  4. Dort auf Benachrichtigungen bearbeiten klicken.
  5. Die oberen zwei Einstellungen dienen nur der Terminerinnerung, also wenn man über einen anstehenden Termin vorher auch noch mal eine Benachrichtigung erhalten möchte. Über die anderen Einstellungen könnt ihr euch entsprechend über neue Termine informieren lassen: Google-Kalender Benachrichtigungen 2
  6. Zuletzt nicht vergessen auf Speichern zu klicken und dann war es das auch schon.

Informationen zur Versionsverwaltung


Wohl kaum ein Projekt in der Informatik kommt heute noch ohne Quellcodeverwaltung aus. Mit Hilfe von Konzepten wie Commits, Branches und Merges ist es für Teams leicht möglich unabhängig zu arbeiten, Änderungen ihres Codes zu verfolgen oder unterschiedliche Versionen zu vereinen.
Daher ist es natürlich auch in unserem DV-Projekt ein fester Bestandteil. Im Folgenden möchte ich euch eine Übersicht insbesondere mit folgenden Punkten geben:

  1. Installation der benötigten Software und Einrichtung.
  2. Sinnvoller Umgang mit Branches.
  3. Übereinkünfte für unser Repository.

1. Installation

Zuallererst solltet ihr euch ein entsprechendes Tool besorgen, welches euch als GUI dient, um die Verwaltung und Konfiguration mit Git einfacher vornehmen zu können. Möglich sind hier z. B. TortoiseGit, SourceTree, VisualStudio oder irgendein anderes Tool eurer Wahl. Ich werde SourceTree verwenden und beschreiben.

Nach der Installation müsst ihr zuerst das Repository klonen und euch damit eine lokale Kopie besorgen. In SourceTree dazu unter Klonen / Neu den Online-Pfad (https://github.com/HochschuleKempten/dvprojekt.git) und den Pfad zum Ordner auf eurer lokalen Festplatte eingeben.

Neues Repository in SourceTree anlegen

Danach werden alle Daten des Repositorys auf eure Festplatte geladen. Das kann etwas dauern, da wir bereits zu Beginn über 500 MB haben. Nachdem die Operation abgeschlossen ist, könnt ihr auch schon loslegen und z. B. das SampleProject öffnen und starten.

Solltet ihr bei eurem GitHub-Account zudem einen Nicknamen gewählt haben, so wäre es empfehlenswert, wenn ihr in eurem Profil noch euren (Vor-)Namen eintragen könntet (Settings → Name). Das macht es für alle Teammitglieder einfacher euer Profil zuzuordnen.
Damit bei Commits von SourceTree aus ebenfalls euer Name erscheint, müsst ihr die entsprechenden Daten noch unter Tools → Optionen → Allgemein eintragen:

Benutzernamen in SourceTree einstellen

2. Branches

Neben den Commits im Hauptbereich seht ihr links die vorhandenen Branches des Repositorys. Unter Zweige sind alle lokalen Branches aufgeführt, also diejenigen welche sich auch auf eurem Rechner befinden. Dahingegen sind unter Remotes → origin alle Branches auf dem Server zu sehen.

Branches in SourceTree

Um Dateien zwischen diesen Bereichen auszutauschen, gibt es die Befehle Push (Daten auf den Server spielen) und Pull (aktualisierte Daten vom Server holen).

Wenn ihr einen neuen Branch erzeugt (Projektarchiv → Zweig), wird dieser erst einmal nur lokal gespeichert. Möchtet ihr ihn auch anderen Teammitgliedern zur Verfügung stellen, so müsst ihr den Branch erst auf den Server pushen (Projektarchiv → Push). Lokale Branches sind insbesondere für eigene, unabhängige Tests oder Refactoringmaßnahmen sinnvoll, also wenn ihr z. B. etwas ausprobieren möchtet ohne dabei einen funktionierenden Stand zu verlieren.

Grundsätzlich solltet ihr immer auf eurem eigenen Branch bzw. auf dem Branch eurer jeweiligen Gruppe arbeiten. Es ist allerdings durchaus sinnvoll, sich ab und zu den aktuellen Stand des master-Branches zu holen. Andere Mitglieder haben vielleicht inzwischen Änderungen eingepflegt, welche auch für euren Stand relevant sind.
In SourceTree müsst ihr dazu erst einmal den aktuellen Stand des master-Branches holen. Wechselt dazu auf diesen Branch (Doppelklick im lokalen Zweig) und startet einen Pull-Request. Anschließend wieder auf euren eigenen Branch zurückwechseln und dann über einen Rechtsklick auf den master-Branch mit dem Befehl Zusammenführen von master in den aktuellen Zweig den Merge starten.

3. Übereinkünfte für unser Projekt

Jede Woche werden im Rahmen der Scrum-Projektumsetzung neue Aufgaben erstellt. Jede Aufgabe bekommt einen Namen und dafür sollte dann ein eigener Branch angelegt werden. Auf diesem Branch könnt ihr dann abgeschottet von den anderen Teammitgliedern arbeiten. Sobald ihr mit eurer Arbeit fertig seid und euren Stand gerne in dem master-Branch sehen würdet, müsst ihr einen Pull-Request starten.
Am einfachsten geht das über die Oberfläche von GitHub selbst. In der Übersichtsseite des Repositorys wechselt ihr auf euren Branch und klickt dann auf den grünen Button, um einen Pull-Request zu starten. Unter base sollte dabei immer master und unter compare immer euer eigener Branch stehen (genauere Informationen sind auch bei GitHub selbst zu finden). Der Integrator des Projektes (ich) wird dann informiert und kann euren Stand in den master-Branch einpflegen.
Bitte beachtet, dass ihr nur einen Pull-Request starten könnt, wenn ihr euren Branch zuvor auf den Server geladen habt.

Hier nochmal die einzelnen Schritte zusammengefasst:

  1. Neuen Branch ausgehend vom master-Branch erzeugen und einen passenden Namen wählen.
  2. Eure Änderungen programmieren.
  3. Euren Branch auf den Server pushen.
  4. Auf GitHub gehen und für euren Branch einen Pull-Request zum master-Branch starten.

Achtung: Da ihr auch Schreibrechte im Repository besitzt, könntet ihr theoretisch den Pull-Request selber durchführen. Dies bitte nicht machen, da man sich ansonsten den ganzen Vorgang hätte sparen können. D. h. nur den Pull-Request starten, aber nicht mergen (konkret nicht auf den Button Merge Pull-Request drücken). Ihr seid fertig, wenn ihr einen Screen ähnlich dem Folgenden seht.

Pull-Request auf GitHub durchführen

Die einzige Möglichkeit diese Berechtigung technisch zu entziehen wäre den Umweg über Forks zu gehen, was ich für unser Projekt für einen übertriebenen Verwaltungsaufwand halte.

Falls irgendwelche Fragen oder Unklarheiten auftauchen stehe ich natürlich jederzeit zur Verfügung.