View on GitHub

GE-Kempten.github.io

Inoffizieller Wiki des Studiengangs Informatik Game Engineering and der Hochschule Kempten

Programmieren 1 für Games

Kompetenzen

Lernergebnis

Am Ende des Semesters sollen die Stunden in der Lage sein mit der IDE zu arbeiten, d.h. sie programmieren, übersetzen den Code und Fehler werden behoben. Durch ständiges Üben ist es ihnen möglich Abhilfe für kleine Probleme der realen Welt mit strukturierten, modularen Programmen zu lösen.

Einführung

Programmierung ist nötig, da der Computer bzw. der Prozessor nur exakt formulierte Aufgabenstellungen ausführen kann. Dieser ist im Resultat aber weitaus effizierter als der Mensch z.B. Ausrechnen von komplizierten Formeln.

Das Schreiben des Programms muss trotzdem der Mensch unternehmen, auch wenn sog. KIs (Künstliche Intelligenz) immer besser werden, wodurch sie das immer stärker übernehmen.

Problem ist nun leider, dass zwischen Mensch und Maschine eine Sprachbarriere ist. Wir Menschen sprechen die natürlich Sprache, welche während dem Gesprächs die Fähigkeit hat Informationen zu interpretieren und dadurch Ironie etc. versteht. Der Computer spricht in der Maschinensprache, welche binören Codes ließt und dadurch nur 0en und 1er versteht.

Als Schnittstelle der beiden Parteien bieten sich Programmiersprachen an, da diese für Beide verständlich sind und dadurch einen guten Kompromiss darstellen.

Dadurch entsteht folgendes Vorgehen:

Anweisungen / Befehle

Anweisungen und Befehle sind unterschiedlich von Programmiersprache zu Programmiersprache, aber grundlegende Konzepte beliben identisch.

Zuweisung:

Kontrollstukturen:

Prozedur-/Funktionsaufrufe:

Programmiersprachen

Große Anzahl von Programmiersprachen, welche nach bestimmten Kriterien unterscheidbar sind:

Kompilierte vs. Interpretierte Sprachen

Kompilierte Sprachen:

Interpretierte Sprachen:

Just-in-Time kompilierte Sprachen (Zwischensprache):

Portabilität:

High vs. Low Level Sprachen

Das Level beschreibt die Nähe/Ähnlichkeit zur Maschinensprache des Systems:

Je ähnlicher, desto lower:

Je abstrakter/universeller/für den Menschen besser lesbarer, desto “higher”:

=> Programmierer muss selbst entscheiden, welche Sprache er für sein Projekt benötigt:

Assembler

ist die erste Programmiersprache und ähnelt der Maschinensprache, ist aber für den Menschen noch lesbar. Ein Veranschaulichung des Codes bietet dieser Übersetzer von C++ in Assembler. Dies ist auch Thema in der Vorlesung IT-Systeme - Assemblerprgrammierung.

Kommunikationsprotokolle

Auch wenn der Mensch mit dem Computer mit Programmiersprachen kommuniziert, heißt das nicht das Computer gegenseitig das Gleiche machen. Sie benutzen Kommunikationsprotokolle, wie z.B. TCP, IP, HTTP, FTP, SMTP, …

Syntax und Semantik

Syntax:

Semanik:

Game-Engineering

Mit Game-Engine kommt man alleine schon recht weit, indem man “Featuren zusammenklickt”, aber: die eigene Innovation wird dadurch eingeschränkt durch die Fähigkeit der Engine.

Viele Entwicklungsstudios entwickeln deshalb (und aus anderen Gründen) ihr eigene Engines.

Durch die eigenen Programmierkenntnisse können so spezielle Grafikeffekte, Interfaces/Controler, Charaktere (KI) und vieles mehr entwickelt und benutzt werden.

Außerdem werden neue Spielansätze und Anforderungen von herkömmlichen Engines noch gar nicht unterstützt. z.B. Kompatibilität zu VR, AR, …

C und C++

C

wurde von Dennis Ritchie zwischen 1969-1973 in den Bell Laboratories, NJ, USA entwickelt, um das neue Betriebsystem UNIX zu programmieren und diente als Vorbild für andere von C insperierte Sprachen. C ist eine prozedurale Sprache.

C++

wurde von Bjarne Stroustrup und seinen Mitarbeitern in den Bell Laboratories (AT & T, USA) entwickelt, um effizienter implementieren zu können. Frühe Versionen, wie “C mit Klassen”, gibt es seit 1980. Ab 1989 wurde mit der Standardisierung von C++ begonnen, um Sprachdialekte zu vermeiden, so wurde dann 1998 der ISO Standard CD 14882 für C++ verabschiedet.

Aus dem Namen ist schon ersichtlich, dass C++ aus C entstand, dabei wurden sogar die kompletten Eigenschaften übernommen.

Eigenschaften von C:

Eigenschaften von C++:

Warum C und C++

Versionen und Standardisierung

Erstes Programm

Zunächst wird eine Integrated Development Environment (IDE) benötigt, da viele Funktionen für den Entwickler bereitstellt und somit die Arbeit an den Code angenehmer macht. Wichtige Funktionen sind Projekte aufsetzen, Quellcodeeditor, Fehlersuche, Kompilieren und Linken, etc.

Das Programm wird aus mehreren Dateien zusammengesetzt**:

Ein simples Programm für den Einstieg ist das sog. Hello World!-Projekt.

Hello World

“Hello World” gibt einen kurzen Einblick in viele Funktionen einer Programmiersprache, da viele simple, aber allgemein wichtige Anweisungen aufgeführt werden.

#include <iostream>

using namespace std;

int main(){
	std::cout << "Hello World!" << endl; //endl gehört auch zu std
	cout << "Oder auch: Hallo Welt!\n";

	return 0;
}

Präprozessordirektive: z.B. #include <header> oder _#include “header”

Diese wird eingeleitet durch ein # und wird ausgeführt am Anfang der Kompilierphase erzeugt noch keine Objektdatei, sondern bündelt Informationen und bereitet diese vor.

Das *include+ gibt an, dass Dateien in das Projekt bzw. den Quellcode ingebunden werden. Allgemein gesagt: <header> bindet Standardbibliotheken ein und “header” eigen erstelle Dateien. Mehr dazu (sehr viel) später.

iostream: Ist zuständig für die Ein und Ausgabe auf der Konsole.

int main: Aufruf der Hauptfunktion.

Die Hauptfunktion ist Pflicht für jedes Programm, da diese Funktionen und Anweisung aufruft. Man könnte sie auch einen roten Pfad nennen, da hier alles zusammenführt und nach der Reihe ausgeführt werden. Das return 0; zeigt, dass das Programm den Exit-Code zurückgibt und ist ebenfalls Notwendigfür alle Funktionen mit einem Rückgabewert. Mehr dazu unter “Funktionen”.

Funktionsblock: { inhalt }

In dem Funktionblock befindet sich alles, was das Programm berechnen/ausführen soll.

Kommentare //Inhalt oder /* Inhalt */

Kommentare werden vom Compiler ignoriert und können dadurch benutzt werden, den Code übersichtlicher zu machen. Dies ist besonders praktisch für das Verständnis anderer Personen, da diese den Code dadurch schneller verstehen. Den es gilt: Teamwork OP!

// wird alles ignoriert -> Einzeiler-Kommentar

/* -> Beginn eines Kommentarblocks */ -> Ende eines Kommentarblocks

Anweisungen enden immer mit einem Semicolon/Strichpunkt ;

Konsolenausgabe:

Da wir iostream eingebunden haben, können wir auf der Konsole des Computers schreiben und auslesen.

std:: kennzeichnet den Namensbereich, indem cout und endl definiert sind. Da wir using namespace std; angegeben haben, muss std:: nicht benutzt werden.

cout (console output) ist eine Funktion der Standardbibliothek, die die Ausgabe auf der Konsole ermöglicht.

” Inhalt “ ist ein String und dessen Inhalt wird nach cout « ausgegeben.

« ist ein Operator, der die Zeichen in den sogg. outputstream schiebt.

endl (end of line) ist ebenfalls eine Funktion der Standardbibltiothek und ruft einen Zeilenvorschub hervor. Alternativ kann in dem String “\n” eingefügt werden, welcher den gleichen Effekt erzeugt.

Elementare Datentypen - Integer und Strings

Ein wichtiger Bestandteil jedes Programmes sind Variablen. In diesen können verschiedene Werte gespeichert und wieder benutzt werden. Um Variablen zu speichern, müssen wir davor noch angeben, wie viel Speicher sie brauchen. Das ist abhängig von der Art des Wertes - also Zahl oder Buchstabe/n - und dem Wertebereich.

Deklaration und Defintion

Deklaration: Wir legen Datentyp und Name fest, dadurch kann der Compiler Speicher reservieren, aber die Variable hat zurzeit noch keinen festgelegten Wert.

Syntax: Datentyp Variablenname;

Namensregeln:

Empfehlung für Namensregeln:

Allgemein sollte man darauf achten, dass man Variablen schnell verständliche Namen gibt. Am Besten auch gleich einen Kommentar anhängen, um zu erklären, was die Aufgabe der Variable ist. Es haben sich im Laufe der Zeit verschiedene Regeln zur Namensgebung entwickelt:

Anmerkung: Benutzen wir die Variable jetzt, dann wird ein zufälliger Wert ausgegeben, falls der Compiler das überhaupt zulässt.

Defintion: Nun wird der Variablen ein Wert zugewiesen. Natürlich muss davor aber erstmal ein Datentyp festgelegt sein.

Syntax: Variablenname = einWert;

Es ist möglich Defintion und Deklaration zu kombinieren.

int eineZahl = 44; //Definition und Deklaration
int eineAndereZahl(21);

int x_pos, y_pos, z_pos; //Deklaration von drei Variablen

x_pos, y_pos, z_pos = 5; //Defintion durch verkettete Wertezuweisung.

//Aber:

int Zahl1, Zahl2, Zahl3 = 3; //Nur Zahl3 wird 3 zugewiesen

int nochEineAndereZahl(22), eineGanzAndereZahl(42);

Integer - Ganze Zahlen

I.d.R. hat ein Integer 4 Byte (16 Bits) Speicherplatz. Integer können aber verschiedene Formen annehmen, welche diese Tabelle darstellen soll:

Datentyp Speicherplatz (Byte) Wertebereich Notiz
bool 1 1,0 bzw. true, false Sehr viel Speicher für 2 Ziffern
char 1 -128, +127 Benutzung für ASCII
short 2 -32.768, +32.767 Hälfte von Integer
integer 4 -2.147.483.648, +2.147.483.647 Alternativer Name: long
long long 8 -9.223.372.036.854.775.808, +9.223.372.036.854.775.807 Doppeltes von Integer

Mit Ausnahme von bool kann an jeden Datentyp noch ein unsigned (ohne Vorzeichen) angehängt werden, um den Wertebereich auf 0 zu verschieben. z.B. unsigned short -> 0, 65.535

Für die Umrechnung von Dezimalzahlen in Binärzahlen solle ein Blick in die Vorlesung “Einführung in die Informatik” gemacht werden.

Overflow

Definition: Der Absolutbetrag des Ergebnisses einer Berechnung oder des Wertes einer Wertzuweisung ist außerhalb des Wertebereichs des Zieldatentyps.

Problem: char hat nur einen Byte zur Verfügung und kann dadurch nicht mehr Zahlen außerhalb des Wertebereichs (z.B. -129) anzeigen.

Lösungsansätze: Modifizierer unsigned und signed

z.B. signed char:

127 -> 0111 1111 128 -> 1000 0000, aber: 1st bit gibt Vorzeichen an: also = -128 129 -> 1000 0001, aber: … = -127 …

unsigned char:

127 -> 0111 1111 128 -> 1000 0000 … 255 -> 1111 1111 256 -> 1 0000 0000 -> Nicht mehr Anzeigbar mit char

Ohne Angabe eines Modifizierers ist der Datentyp mit Vorzeichen. Man sollte allgemein als Entwickler überblick über die Werte seiner Variablen behalten.

Mögliche Overflow-Gefahren sind:

Ein weiteres Problem: Der Integer-Wertebereich ist System-individuell:

Hilfestellung 1: Die Funktion sizeof() gibt die Anzahl der Bytes aus

cout << "long: " << sizeof(long) << "Bytes" << endl;

Hilfestellung 2: Die Konstanten in climits geben Wertebereiche zurück

cout << "Wertebereich - long long: " << LLONG_MIN << ", " << LLONG_MAX << endl;

Float - Gleichkommazahlen

Datentyp Speicherplatz (Byte) Wertebereich Genauigkeit(Stellen)
Float 4 ±3.4E+38 6
Double 8 ±1.7E+308 15
Long Double 10 ±1.1E+4932 19

Anmerkung zu Genauigkeit: D.h. das insgesamt so viele Stellen angezeigt werden können

float eineZahl = 20.0001f; 
cout << eineZahl << endl; //20.0001
eineZahl = 20.00009f; 
cout << eineZahl << endl; //20.0001, 7 Stellen, aber es werden nur 6 angezeigt

Underflow

Bei Gleitkommazahlen kann es vorkommen, dass der wahre Wert einer Berechnung zu klein ist (zu viele Nachkommastellen) um durch Datentypen dargestellt zu werden.

Praktischerweise können wir ebenfalls sizeof() und climits benutzen, um Speicherplatz und Wertebereich zu überprüfen.

#include <climits> //oder <cfloat>

float fUnderflow = DBL_MIN; //Kleinster double Wert wird einem Float zugewiesen

cout << "DBL_MIN: " << DBL_MIN << endl; //2.22507e-308
cout << "fUnderflow: " << fUnderflow << endl; //0 -> Underflow

Konstanten

Eine Konstante ist eine Zahl, ein boolesches Schlüsselwort, ein zeichen oder eine Zeichenkette/String.

Ganzzzahlige nummerische Konstanten

sind i.d.R. von Typ int. Sollte der Wert zu groß für den Datentyp sein, so wird diese erweitert, d.h. sie wird zu long, und dann zu long long.

Den Datentyp ist auch erkennbar durch die Schreibweise. Beispiele:

Zahlen können auch in verschiedenen Zahlensystemen angezeigt werden:

Möchte man Konstanten in andere Zahlensysteme umwandeln, so kann man die Operatoren oct, hex und dec in Kombination mit dem Outputstream « benutzen.

cout << "16 in Hexadezimal: " << hex << 16 << endl;

Gleitpunktkonstanten

sind Standardmäßig von Typ double können aber mit als

Konstante Objekte

Konstanter Datentyp

const int eineZahl = 44;

Durch das Schlüsselwort const vor dem Datentyp wird diese als Konstante deklariert. Folglich ist diese nicht überschreibar, da der Compiler sie schützt. Anwendung finden sie z.B. bei der maximalen Anzahl von Spielern.

Präprozessor

Syntax: #define TEXZERSETZUNG Inhalt

#define EINE_ZAHL 44

Durch das Schlüsselwort define hinter der Präprossessordirektive, werden alle Textstellen hinter dem #define mit dem Inhalt ausgetauscht. GROSSSCHREIBUNG hat sich als Erkennungsmerkmal eingebürgert.

Datentyp bool

bool updateReady = true;

Datentyp char

Zwar können char Variablen auch benutzt werden, um Zahlen zu speichern, ist es eigt. gedacht diese für einzelne Zeichen zu benutzen. Um diese Darzustellen, gibt es verschiedene Zeichensätze:

Möchte man nun ein Buchstabe in einer Variable speichern, so muss dieser in ’ ‘ gesetzt werden.

char BuchstabeA = 'A';

Letztendlich ist die Zielausgabeeinheit - i.d.R. der Bildschirm - zuständig für die korrekte Ausgabe.

ASCII

ASCII

ANSI

Erweiterung der ASCII-Tabelle um einen Bit. Nun sind z.B. deutsche Umlaute darstellbar.

ANSI

char einCoolesZeichen = 0xCE;gm



### Unicode

Da char nur 8 Bit hat, kann der Unicode nicht mehr dargestellt werden. Deswegen müssen alternative Datentypen her: **char16_t** (2 Byte), **char32_t** (4 Byte) (*wchar_t* ist veraltet). 





## Datentyp string

Strings sind **Zeichenketten**, welche das Arbeiten viel einfacher machen als mit chars. Um diese anzugeben, werden **" "** benutzt.

Die Zeichenkette endet inter immer mit dem **Stringendezeichen \0**, welches 1 Byte benötigt. Folglich ergibt dies für den **Speicherbedarf**: **Anzahl der Zeichen + 1**.

I.d.R. sind strings im **iostream** integriert, aber es kann alternativ **<string>** eingebunden werden.

```c++
#include <iostream> //<string>
unsing namespace std;
string einName = "Hans"; //std::string
cout << "Hallo ich bin " << einName << "." << endl;

Umlaute

Umlaue müssen aus dem ANSI Zeichesatz entnommen werden, dies kann entweder über einer Zuweisung des Zeichencodes an einem char passieren, oder als hex code an einer Stringkonstante

Zeichen Hex
ä 84
Ä 8E
ö 94
Ö 99
ü 81
Ü 9A
ß E1
//cout << Üben << endl; //geht nicht
cout << \x9aben << endl; //Üben

char UE = 9a;
cout << UE << "ben" << endl; //Üben

Funktionen mit Strings

Ein string ist eine Klasse, in welcher verschiedene Funktionen integriert wurden. Mehr über Klassen in der OOP.

TODO: Verlinkung OOP

Suchen: string.find() -> Gibt (Start-)Position einer gesuchten Zeichens/-kette zurück.

Vier Varianten:

size_t find (const string& str, size_t pos = 0) const; //2 Parameter, wovon nur der erste angegeben werden muss
size_t find (const char* s, size_t pos = 0) const; //Same here
size_t find (const c, size_t pos = 0) const; //Again, same here
size_t find (const char* s, size_t pos, size_t n) const; //3 Parameter
string stringA = "Ich bin ein String!";
string stringB = "String";

size_t SucheString = stringA.find(stringB); //Suche "String" ab 0ter Stelle -> Zahl als Ergebnis

string::npos: npos ist eine Konstante für den höchsten Wert von size_t. Nutzbarkeit findet diese für die Abfrage einer erfolgreichen Suche.

if(SucheString != string::npos){ //Verzweigung nur wenn StringB in StringA gefunden wurde
	cout << stringB << " wurde an der " << SucheString << "ten Stelle gefunden." << endl; 
}

Erweiterung von Strings: Anhängen weiterer Zeichen an einen String

stringC = "Zeichenkette";

stringA += "Ich ";
stringA.append("bin ");
stringA.push_back("eine ");
stringA.append(stringC);
stringA += "! Liebe ";
stringA.push_back("mich!!!");

cout << stringA << endl; //Ich bin ein String! Ich bin eine Zeichenkette! Liebe mich!!!

Länge: string.length() -> gibt Länge des Strings als Zahl zurück.

stringA.length; //60

Ersetzen: string.replace() -> ersetze eine Anzahl von Zeichen mit anderen

stringA.replace(stringA.find(stringC) + stringC.length() + 2, 13, "Ich bin praktisch!");

cout << stringA << endl; //Ich bin ein String! Ich bin eine Zeichenkette! Ich bin praktisch!

Einfügen: string.insert() -> Füge einen String an einer bestimmten Position ein.

stringA.insert(stringA.find(stringB) + stringB.length() + 2, "D.h. ");

cout << stringA << endl; //Ich bin ein String! D.h. Ich bin eine Zeichenkette! Ich bin praktisch!

Teilstring: string.substr() -> Nimm ein Teil von einem String

string stringD = stringA.substr(stringA.find(stringB) + stringB.length() + 2, stringA.find(StringC));

cout << stringD << endl; //D.h. Ich bin eine Zeichenkette!

Natürlich gibt es noch viele weitere Funktionen von strings. Mehr dazu auf cplusplus.

Umwandlung: Zahlen <-> String:

string to:

Parameter: ein String

Alternative Möglichkeit: atoi, etc. aber diese möchten ein char-Array

number to string: to_string(val), val = number

int eineZahl = 42;

string einString = to_string(eineZahl); //42

int eineAndereZahl = stoi(einString); //42

Arithmetische Operatoren

Hinweis

Hinter jeder Operation muss hinterfragt werden, ob der Datentyp passt, d.h. wenn als Ergebnis einer Operation eine Gleitpunktzahl entsteht, muss der Datentyp der Variable auch passen.

Variable können auch zum Rechnen benutzt werden.

Binäre arithmetische Operatoren

Linker Operand Operator Rechter Operand
A + B
Operator Bedeutung Beispiel
+ Addition 2 + 6 = 8
- Subtraktion 2 - 6 = -4
* Mutiplikation 2 * 6 = 12
/ Division 2 / 6 = 0.33333
% Modulodivision 2 % 6 = 2

Division

Wie oben bereits erwähnt muss ein entsprechende Datentyp angegeben werden, damit man ein gewünschtes Ergebnis erzielt.

int var1 = 2 / 6; //= 0
float var2 = 2 / 6: //= 2.00000

int var3 = 2.f / 6; //= 0 
float var4 = 2 / 6.f; //= 0.33333 

//Am besten immer .f hinter allen ganzzahligen Zahlen bei Gleitkommazahlen-Berechnungen setzen!

Modulodivision

Nur anwendbar auf ganzzahilge Operanden.

Beispiel: x % 4 = erg

x erg
0 0
1 1
2 2
3 3
4 0
5 1
6 2
7 3
8 0
9 1
10 2

Anwendung: Erzeugung von Zufallszahlen in einem bestimmten Wertebereich -> Thema später

Ausdrücke

Natürlich können wir auch mehrere Operanden und deren Operatoren kombinieren. Dabei gelten die üblichen Rechenregeln -> Punkt vor Strich, etc.

Eine übersicht welche Operatoren Priorität über andere haben wir zum Schluss des Kapitels gezeigt.

Unäre arithmetische Operatoren

Operator Bedeutung Beispiel
+ und - Vorzeichenoperator x = 5; y = -x; //y = -5
++ Inkrementoperator x = 2; x++; //x = 3
Dekrementoperator x = 2; x–; //x = 1

Inkrement- und Dekrementoperatoren

verändern den Wert um 1.

Unterscheidung zwischen:

int var1 = 2;
int var2 = var1++; //var2 = 2
int var3 = ++var1; //var3 = 4

Zusammengesetze Zuweisungen

Es besteht die Möglichkeit den Zuweisungsoperator “=” mit den binären Operatorn zu kombinieren.

-> +=, -=, *=, /=, %=

int var1 = 2;

var1 = var1 + var1; //var1 = 4
var1 += var1; //var1 = 8

Vergleichsoperatoren

Operator Bedeutung Bespiel Ergebnis
< Kleiner 4.5 < 5.623 true
<= Kleiner gleich 2 <= 1.98 false
> Größer    
>= Größer gleich    
== Gleich 2*2 == 5 false
!= Ungleich 2*2 != 5 true

Hinweis

Nicht die Zuweisung “=” mit dem Vergleichsoperator gleich “==” verwechseln!

Boole’sche Operatoren

Konjunktion &&

&& 0 1
0 0 0
1 0 1

-> Das Monster hat unter 50% seiner HP und hat keine MP dann heilt es sich.

Disjunktion ||

oder 0 1
0 0 1
1 1 1

-> Der Spieler ist verletzt oder setzt Block ein dann aktivieren sich die Invincibility-Frames.

Konjunktionen haben Priorität über Disjunktionen.

Negation !

  !
0 1
1 0

-> Der Spieler ist nicht im Empfohlenen Levelbereich dann greifen die Monster nicht an.

Werden Boole’sche Operatoren in Kombination mit Integer/Float-Zahlen kombiniert, dann sind alle Zahlen >/< 0 -> 1 und 0 -> 0

Vorrang der Operatoren

Prioriät Operator
9 Postfix ++
  Postfix –
8 ++ Präfix
  – Präfix
  + Vorzeichen
  - Vorzeichen
  !
7 *
  /
  %
6 +
5 <
  <=
  >
  >=
4 ==
  !=
3 &&
2 \mid\mid
1 =
  *=, /=, +=, -=,