|
Tutorial 10 - Neue Waffen
von AssKicka
In diesem Tutorial lernst du, wie man eine neue Waffe inklusive Modell, Shader,
Skin und Verhalten in Q3A integriert. Wir benutzen dazu den Flammenwerfer
der Modifikation Solidground
2.0. Dies ist ein sehr langes Tutorial, das
viele Änderungen und Zusätze in cgame, ui und game benötigt, also hole
dir erstmal eine Cola und dann fang an zu lesen...
1. Waffen- und Munitionsdefinitionen hinzufügen
Zuerst werden wir den Flammenwerfer und die daraus resultierende Todesart
MOD (means of death) in die entsprechenden Aufzählungstypen einfügen. Öffne bg_public.h und ergänze die folgende Zeile in weapon_t (Z 295):
WP_GRAPPLING_HOOK,
#ifdef MISSIONPACK
WP_NAILGUN,
WP_PROX_LAUNCHER,
WP_CHAINGUN,
#endif
WP_FLAME_THROWER,
WP_NUM_WEAPONS
} weapon_t;
Neben den einzelnen Waffen enthält der Aufzählungstyp weapon_t als ersten Wert WEAPON_NONE und als letzten Wert WP_NUM_WEAPONS.
Aus diesen Werten werden an anderer Stelle im Code die Anzahl der Elemente
von weapon_t (also der Waffentypen) bestimmt, ebenso der Wertebereich
für Variablen dieses Typs. Das funktioniert aber nur, wenn sich
alle Waffen zwischen diesen beiden Werten befinden.
Weiter unten fügen wir dem Typ meansOfDeath_t folgende
Zeile hinzu um später die Todesart mit einer entsprechenden Textmeldung
auswerten zu können (Z 570).
MOD_GRAPPLE , // <- komma
MOD_FLAME_THROWER
} meansOfDeath_t;
Nun öffne die Datei g_combat.c. Zum Loggen der
Ereignisse (später zu besichtigen in games.log) müssen wir
in char *modNames[] folgendes hinzufügen (Z 290):
"MOD_GRAPPLE" , // <- komma
"MOD_FLAME_THROWER"
};
Die letzten beiden Ergänzungen müssen an identischen Positionen
vorgenommen werden, da der Aufzählungstyp benutzt wird, um den
Index im char* Feld zu ermitteln.
Nun müssen wir die Indizes fürs Inventory und das Modell
einfügen. Öffne inv.h und füge die folgende beiden
Zielen hinzu(Z 55):
#define INVENTORY_FLAMETHROWER 50
und (Z 130):
#define MODELINDEX_FLAMETHROWER 52
Falls diese Werte schon vergeben sind, benutze den nächstmöglich
grösseren.
Als nächstes fügen wir die Definitionen für die neue
Waffe und ihre Munition ein. Öffne bg_misc.c und
füge am Ende von bg_itemlist[] diese Zeilen ein: (Z 900)
#endif
/*QUAKED weapon_flamethrower (.3 .3 1) (-16 -16 -16) (16 16 16) suspended
*/
{
"weapon_flamethrower",
"sound/misc/w_pkup.wav",
{ "models/weapons2/flamethrower/flamethrower.md3",
0, 0, 0},
/* icon */ "icons/iconw_flame",
/* pickup */ "flame thrower",
20,
IT_WEAPON,
WP_FLAME_THROWER,
/* precache */ "",
/* sounds */ ""
},
// end of list marker
{NULL}
};
Was hat das alles zu bedeuten? Mit diesen Werten wird eine Variable
des Typs gitem_t gefüttert.
1) Die erste Zeile muss so aussehen und so stehen bleiben, auch wenn
es nur ein Kommentar ist. Der Quake3 Leveleditor benutzt diese Information,
wenn man die Waffe in eine Map als einsammelbares Item setzt.
2) weapon_flamethrower ist der Name unserer Waffe
3) "sound/misc...." ist der Sound, der beim Einsammeln
der Waffe abgespielt wird
4) "models/weapons2..." ist das 3D-Modell, das als Waffe
benutzt werden wird. Wir nehmen zu diesem Zweck den Flammenwerfer der
Mod Solidground 2.0.
5) "icons/iconw...." ist das Icon, das bei der Auswahl
der Waffe als aktuelle Waffe angezeigt wird
6) "/* pickup */...." ist der Name, der am Bildschirm beim
Einsammeln der Waffe erscheint
7) "20,..." ist die Menge an Munition, die man beim Aufsammeln
erhält
8) "IT_WEAPON..." identifiziert das Item als Waffe
9) "WP_FLAME_THROWER..." Details zum Itemtyp, also: eine
Waffe vom Typ Flamenwerfer
Als nächstes werden wir die Munition hinzufügen, die vom
Flammenwerfer benutzt wird. Wir verwenden dafür einfach das Model
der BFG Munition. Füge folgende Zeilen direkt unter obigem Code
ein.
/*QUAKED ammo_flame (.3 .3 1) (-16 -16 -16) (16 16 16) suspended
*/
{
"ammo_flame",
"sound/misc/am_pkup.wav",
{ "models/powerups/ammo/bfgam.md3",
0, 0, 0},
/* icon */" icons/icona_bfg",
/* pickup */ "Flame Ammo",
50,
IT_AMMO,
WP_FLAME_THROWER,
/* precache */ "",
/* sounds */ ""
},
// end of list marker
{NULL}
};
Die Bedeutung des Codes ist nahezu analog zu dem Obigen. Das einzig interessante
ist, dass "WP_FLAME_THROWER..." hier angibt, für welche Waffe die
Munition ist.
2. Die Feuerrate einstellen
Da unsere Waffe ein Flammenwerfer ist, wollen wir, dass er eine sehr
hohe Feuerrate hat. Um dies zu bewerkstelligen fügen wir in der
Funktion PM_Weapon in bg_pmove.c folgendes hinzu
(Z 1660):
case WP_GRAPPLING_HOOK:
addTime = 400;
break;
case WP_FLAME_THROWER:
addTime = 40;
break;
#ifdef MISSIONPACK
Je niedriger der Wert von addtime ist, umso höher ist die
Feurrate!
3. Den Spieler mit Flammenwerfer spawnen
Wir müssen den Flammenwerfer als Standardwaffe für jeden
respawnenden Spieler einstellen, da es momentan keine Maps gibt, in
denen unsere neue Waffe geladen wird. Öffne die Datei g_client.c und ergänze in der Funktion ClientSpawn (Z 1170):
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_GAUNTLET );
client->ps.ammo[WP_GAUNTLET] = -1;
client->ps.ammo[WP_GRAPPLING_HOOK] = -1;
client->ps.stats[STAT_WEAPONS] |= ( 1 << WP_FLAME_THROWER );
client->ps.ammo[WP_FLAME_THROWER] = 999;
Mit der ersten Zeile haben wir das Bit für WP_FLAME_THROWER
im Inventory des Spielers gesetzt, in der zweiten haben wir die Munition
auf 999 gesetzt, da es momentan auch noch keine Maps mit unserer Ammo gibt.
Wobei wir aber schon das Item "flame_ammo" definiert haben...
Zudem müssen wir registrieren, mit welchen Items der Spieler startet.
Öffne g_items.c und schreibe in die Funktion ClearRegisteredItems folgendes (Z 785):
// players always start with the base weapon
RegisterItem( BG_FindItemForWeapon( WP_MACHINEGUN ) );
RegisterItem( BG_FindItemForWeapon( WP_GAUNTLET ) );
RegisterItem( BG_FindItemForWeapon( WP_FLAME_THROWER) );
#ifdef MISSIONPACK
if( g_gametype.integer == GT_HARVESTER ) {
4. Den Flammenwerfer nicht ablegen
Wir wollen verhindern, dass der Flammenwerfer abgelegt wird, wenn der Spieler
gefraggt wird. Das ist nicht zwingend notwendig, aber es bietet sich
zu Lehrzwecken gut an :o). Öffne die Datei g_combat.c und
ändere in TossClientItems folgenden Code (Z 70):
if ( weapon == WP_MACHINEGUN || weapon == WP_GRAPPLING_HOOK || weapon == WP_FLAME_THROWER) {
if ( self->client->ps.weaponstate == WEAPON_DROPPING ) {
weapon = self->client->pers.cmd.weapon;
}
Wenn ein Spieler gefraggt wird, so bleibt normalerweise diejenige Waffe
liegen die er gerade ausgewählt hat. Wird er aber während
des Waffenwechsels gefraggt, so wird unterschieden: ist die Waffe von
der er wechselt die Machinegun oder Grappling Hook ( oder Flammenwerfer)
so wird die Waffe aktiviert, zu der der Spieler wechseln wollte. Andernfalls
wird die Waffe aktiviert, von der der Spieler wechseln wollte.
Direkt darunter folgt der Code zum ablegen der Waffe, ergänze folgendes:
if ( weapon > WP_MACHINEGUN && weapon != WP_GRAPPLING_HOOK &&
weapon != WP_FLAME_THROWER && self->client->ps.ammo[ weapon ] ) {
// find the item type for this weapon
item = BG_FindItemForWeapon( weapon );
// spawn the item
Drop_Item( self, item, 0 );
}
Hier verhindern wir das Waffen vom Flammenwerfer-Typ abgelegt werden,
genau wie Waffen ohne Munition + MG + Hook + Pummel.
5. Die Waffe abfeuern
Jetzt müssen wir noch Projektile erzeugen und ihnen eine think-Funktionen
spendieren. In der Datei g_weapon.c fügen wir
der Funktion FireWeapon folgendes hinzu (Z 860):
case WP_GRAPPLING_HOOK:
Weapon_GrapplingHook_Fire( ent );
break;
case WP_FLAME_THROWER :
Weapon_fire_flame( ent );
break;
#ifdef MISSIONPACK
case WP_NAILGUN:
Dies wird die Funktion "Weapon_fire_flame" aufrufen (die
wir gleich noch definieren werden), sobald der Spieler den Flammenwerfer
abfeuert.
Wir bleiben in g_weapons.c und fügen direkt unter Weapon_LightningFire(),
aber noch vor den Q3TA Waffen die Funktion Weapon_fire_flame ein (Z 670):
/*
=======================================================================
FLAME_THROWER
=======================================================================
*/
void Weapon_fire_flame (gentity_t *ent ) {
gentity_t *m;
m = fire_flame(ent, muzzle, forward);
m->damage *= s_quadFactor;
m->splashDamage *= s_quadFactor;
}
#ifdef MISSIONPACK
/*
======================================================================
NAILGUN
Das mag erst ein wenig verwirrend aussehen, ist aber
in Wirklichkeit sehr einfach. Zuerst erschaffen wir einen Zeiger auf
ein Entity namens "m" vom Typ gentity_t. Dann rufen wir
die Funktion "fire_flame" auf und übergeben dieser
das Entity des Funktionsaufrufers, den Startpunkt (muzzle) und den
Richtungsvektor (forward).
Die Funktion "fire_flame" wird ein neues Entity
erschaffen (bisher haben wir ja nur den Zeiger dafür angelegt)
und all die Informationen über das neue Entity in m speichern.
Danach multiplizieren wir den Schaden und Splashdamage noch mit dem
Quadfaktor (1 falls inaktiv, 3 falls aktiv). Damit damage / splashdamage
mit etwas multipliziert werden können, müssen natürlich
erst einaml einen Wert bekommen. Genau dafür ist die Funktion fire_flame zuständig. Öffne g_missile.c und füge
folgendes zwischen fire_grapple und den Q3TA Funktionen ein (Z
655):
/*
=================
fire_flame
=================
*/
gentity_t *fire_flame (gentity_t *self, vec3_t start, vec3_t dir) {
gentity_t *bolt;
VectorNormalize (dir);
bolt = G_Spawn();
bolt->classname = "flame";
bolt->nextthink = level.time + 1500;
bolt->think = G_ExplodeMissile;
bolt->s.eType = ET_MISSILE;
bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN;
bolt->s.weapon = WP_FLAME_THROWER;
bolt->r.ownerNum = self->s.number;
bolt->parent = self;
bolt->damage = 30;
bolt->splashDamage = 25;
bolt->splashRadius = 45;
bolt->methodOfDeath = MOD_FLAME_THROWER;
bolt->splashMethodOfDeath = MOD_PLASMA_SPLASH;
bolt->clipmask = MASK_SHOT;
bolt->s.pos.trType = TR_LINEAR;
bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame
VectorCopy( start, bolt->s.pos.trBase );
VectorScale( dir, 300, bolt->s.pos.trDelta );
SnapVector( bolt->s.pos.trDelta ); // Netzbandbreite einsparen
VectorCopy (start, bolt->r.currentOrigin);
return bolt;
}
#ifdef MISSIONPACK
/*
=================
fire_nail
=================
*/
Ok, das sieht erstmal fies aus. Gehen wir die
Funktion im einzelnen durch. (Ich werde nur die Dinge erklären,
die nicht selbsterklärend sind :o)).
Zu beginn erzeugen wir einen Zeiger auf ein neues Entity, genannt "bolt".
Diesen Zeiger werden wir an die Weapon_fire_flame() Funktion zurückgeben.Wir
erzeugen das Entity durch G_Spawn(). Das Entity bekommt den Namen "flame".
Eine Zeile darauf setzen wir die Zeit bis zum nächsten
"Nachdenken" (think) auf 1,5 Sekunden. Das bedeutet, dass
nach 1,5 Sekunden die eine Zeile darunter zugewiesene Think Funktion G_ExplodeMissile aufrufen wird. G_ExplodeMissile wird
eine Explosion erzeugen, wenn innerhalb von 1,5 Sekunden nichts getroffen
wurde.
Nun setzen wir den Entity Typ auf ET_MISSILE, wodurch
in jedem Frame die Funktion G_RunMissile aufgerufen wird. Das
passiert übrigens von der Funktion G_RunFrame aus in g_main.c,
die Entities auf die Eigenschaft ET_MISSILE überprüft
und daraufhin G_RunMissile aufruft.
Jetzt setzen wir das Entity Flag für den Server.
Wenn man ein ET_MISSILE Entity benutzt, muss man unbedingt SVF_USE_CURRENT_ORIGIN und nicht entity->s.origin benutzen!
Der Eigentümer (owner) des Entities wird gesetzt.
Einige Zeilen später setzen wir die "clipmask"
auf MASK_SHOT. Das bedeutet, dass dieses Entity bei CONTENTS_SOLID, CONTENTS_BODY und CONTENTS_CORPSE aufgehalten wird.
Durch "bolt->s.pos.trType = TR_LINEAR;" wird
das Bewegungsmuster auf linear gestellt. Dies bedeutet, dass das Entity
nicht von der Schwerkraft beeinflusst wird.
Nun müssen wir den Prototyp dieser Funktion in g_local.h eintragen. Darin gibt es vorgegebene Bereiche
für die jeweiligen .c Dateien (Z 500).
gentity_t *fire_blaster (gentity_t *self, vec3_t start, vec3_t aimdir);
gentity_t *fire_plasma (gentity_t *self, vec3_t start, vec3_t aimdir);
gentity_t *fire_grenade (gentity_t *self, vec3_t start, vec3_t aimdir);
gentity_t *fire_rocket (gentity_t *self, vec3_t start, vec3_t dir);
gentity_t *fire_bfg (gentity_t *self, vec3_t start, vec3_t dir);
gentity_t *fire_grapple (gentity_t *self, vec3_t start, vec3_t dir);
gentity_t *fire_flame (gentity_t *self, vec3_t start, vec3_t aimdir);
#ifdef MISSIONPACK
gentity_t *fire_nail( gentity_t *self, vec3_t start, vec3_t forward, vec3_t right, vec3_t up );
gentity_t *fire_prox( gentity_t *self, vec3_t start, vec3_t aimdir );
#endif
Alle serverseitigen Funktionen für den Flammenwerfer sind nun implementiert.
6. Effekte und Graphik
Als erstes werden wir unsere neuen Shaderdefinitionen für die
Flammen einbauen. Öffne cg_local.h und füge
zu den vielen qhandle's in cgMedia_t deine eigenen (Z 725):
qhandle_t plasmaBallShader;
qhandle_t waterBubbleShader;
qhandle_t bloodTrailShader;
qhandle_t flameBallShader;
qhandle_t flameExplosionShader;
#ifdef MISSIONPACK
qhandle_t nailPuffShader;
qhandle_t blueProxMine;
#endif
Öffne cg_main.c und füge in CG_RegisterGraphics folgendes ein (Z 845):
cgs.media.plasmaBallShader = trap_R_RegisterShader( "sprites/plasma1" );
cgs.media.flameBallShader = trap_R_RegisterShader( "sprites/flameball" );
cgs.media.bloodTrailShader = trap_R_RegisterShader( "bloodTrail" );
cgs.media.lagometerShader = trap_R_RegisterShader("lagometer" );
cgs.media.connectionShader = trap_R_RegisterShader( "disconnected" );
Wir haben also zwei neue Shader erzeugt ("flameballShader"
und "flameExplosionShader"). Die tatsächliche Shaderfunktion
wird in einer .shader Datei gespeichert (zu finden in tut10.pk3 ->
solidground.shader).
Jetzt fügen wir die Waffeninformationen für den Flammenwerfer
hinzu, wie Sounds, Shader für Explosionen usw. Öffne cg_weapon.c und füge in die Funktion CG_RegisterWeapon an passender
Stelle folgendes ein (Z 690):
case WP_FLAME_THROWER:
weaponInfo->missileSound = trap_S_RegisterSound( "sound/weapons/plasma/lasfly.wav", qfalse );
MAKERGB( weaponInfo->flashDlightColor, 0.6f, 0.6f, 1 );
weaponInfo->flashSound[0] = trap_S_RegisterSound( "sound/weapons/plasma/hyprbf1a.wav", qfalse );
cgs.media.flameExplosionShader = trap_R_RegisterShader( "rocketExplosion" );
break;
Dieser Abschnitt legt in Q3A fest, welche Sounds und Shader
benutzt werden, und wo diese zu finden sind. Wir haben für den
Flammenwerfer den Sound der PlasmaGun benutzt, und den Shader für
Raketenexplosionen (rocketExplosion).
In der gleichen Datei fügen wir in der Funktion CG_MissileHitWall das Fettgedruckte ein (Z 1865):
case WP_SHOTGUN:
mod = cgs.media.bulletFlashModel;
shader = cgs.media.bulletExplosionShader;
mark = cgs.media.bulletMarkShader;
sfx = 0;
radius = 4;
break;
case WP_FLAME_THROWER:
mod = cgs.media.dishFlashModel;
shader = cgs.media.flameExplosionShader;
sfx = cgs.media.sfx_plasmaexp;
mark = cgs.media.burnMarkShader;
radius = 16;
break;
Hier legen wir fest, was geschieht, wenn unser Flammenprojektil auf
etwas Solides trifft. Was bedeutet das alles?
1) mod: Dies ist das Modell, auf das wir unseren Shader "flameExplosionShader" anwenden
2) shader: Das ist der Shader, den wir auf das Modell anwenden
3) sfx: Dies ist der Sound, der bei der Explosion abgespielt wird
4) mark: Der Shader für die "Marks on Wall", also die Schußspuren
5) radius: Der Radius der Explosion
Nun müssen wir die Grafik für die Flammenprojektile
hinzufügen (also für das Flammen-Entity, das wir zuvor gespawnt
haben). Öffne cg_ents.c und füge den folgenden
Code in CG_Missile ein (Z 450):
if ( cent->currentState.weapon == WP_PLASMAGUN ) {
ent.reType = RT_SPRITE;
ent.radius = 16;
ent.rotation = 0;
ent.customShader = cgs.media.plasmaBallShader;
trap_R_AddRefEntityToScene( &ent );
return;
}
if (cent->currentState.weapon == WP_FLAME_THROWER ) {
ent.reType = RT_SPRITE;
ent.radius = 32;
ent.rotation = 0;
ent.customShader = cgs.media.flameBallShader;
trap_R_AddRefEntityToScene( &ent );
return;
}
Hier haben wir angegeben, dass unser Flammenprojektil (Entity)
den "flameBallShader" benutzen soll. Den Entitytypen haben
wir als Sprite definiert, der Radius des Sprites ist 32 und die Rotation
des Entities wird (hier) auf 0 gesetzt. Die Rotation des Projektils
wird schon vom "flameBallShader" durchgeführt, indem
die Textur "flameball.tga" gedreht wird, die wir zuvor erzeugt
haben.
Jetzt fügen wir unsere MOD (Means Of Death) Nachricht hinzu,
die erscheinen soll, wenn ein Spieler von einem Flammenwerfer getötet
wird. Öffne cg_event.c und füge in CG_Obituary folgenden Code hinzu(Z 275):
case MOD_BFG_SPLASH:
message = "was blasted by";
message2 = "'s BFG";
break;
case MOD_FLAME_THROWER:
message = "was fried by";
break;
#ifdef MISSIONPACK
case MOD_NAIL:
message = "was nailed by";
break;
7. Eintrag ins Menü
Zuletzt werden wir das Kommando "weapon 11" hinzufügen,
damit wir diesem eine Taste zuweisen können. Öffne ui_controls2.c (im q3_ui Modul !) und schreibe zwischen die bereits vorhandenen #define-Anweisungen
(Z 85):
#define ID_WEAPON8 24
#define ID_WEAPON9 25
#define ID_WEAPON11 26
#define ID_ATTACK 27
#define ID_WEAPPREV 28
Alle ID_Werte nach ID_WEAPON11 müssen um eins erhöht werden.
Etwas weiter unten (Z 130):
#define ANIM_DIE 24
#define ANIM_CHAT 25
#define ANIM_WEAPON11 26
Einige Zeilen tiefer, in controls_t, kommt diese Zeile
dazu (Z 150):
menutext_s weapons;
menutext_s misc;
menuaction_s flamethrower;
menuaction_s walkforward;
menuaction_s backpedal;
In der g_bindings Struktur weiter unten füge folgendes
ein (Z 245):
{"messagemode4","chat - attacker",ID_CHAT4,ANIM_CHAT,-1,-1,-1,-1},
{"weapon 11","flame thrower",ID_WEAPON11,ANIM_WEAPON11,'f',-1,-1, -1},
{(char*)NULL,(char*)NULL,0,0,-1,-1,-1,-1},
};
Ein paar Zeilen tiefer in g_weapons_controls (Z
295):
(menucommon_s *)&s_controls.plasma,
(menucommon_s *)&s_controls.bfg,
(menucommon_s *)&s_controls.flamethrower,
NULL,
};
In der Funktion Controls_UpdateModel (Z 505):
case ANIM_WEAPON10:
s_controls.playerWeapon = WP_GRAPPLING_HOOK;
break;
case ANIM_WEAPON11:
s_controls.playerWeapon = WP_FLAME_THROWER;
break;
case ANIM_ATTACK:
s_controls.playerTorso = TORSO_ATTACK;
break;
Gleich haben wir es geschafft... in die Funktion Controls_MenuInit kommt folgendes hinein (Z 1385):
s_controls.bfg.generic.ownerdraw = Controls_DrawKeyBinding;
s_controls.bfg.generic.id = ID_WEAPON9;
s_controls.flamethrower.generic.type = MTYPE_ACTION;
s_controls.flamethrower.generic.flags = QMF_LEFT_JUSTIFY|QMF_PULSEIFFOCUS|QMF_GRAYED|QMF_HIDDEN;
s_controls.flamethrower.generic.callback = Controls_ActionEvent;
s_controls.flamethrower.generic.ownerdraw = Controls_DrawKeyBinding;
s_controls.flamethrower.generic.id = ID_WEAPON11;
etwas weiter unten (Z 1605):
Menu_AddItem( &s_controls.menu, &s_controls.rocketlauncher );
Menu_AddItem( &s_controls.menu, &s_controls.lightning );
Menu_AddItem( &s_controls.menu, &s_controls.railgun );
Menu_AddItem( &s_controls.menu, &s_controls.plasma );
Menu_AddItem( &s_controls.menu, &s_controls.bfg );
Menu_AddItem( &s_controls.menu, &s_controls.flamethrower );
Die allerletzte Änderung! Wir benötigen hierzu
in der Datei ui_players.c die Funktion UI_PlayerInfo_SetWeapon (Z 125):
case WP_GRAPPLING_HOOK:
MAKERGB( pi->flashDlightColor, 0.6f, 0.6f, 1 );
break;
case WP_FLAME_THROWER:
MAKERGB( pi->flashDlightColor, 0.6, 0.6, 1 );
break;
default:
MAKERGB( pi->flashDlightColor, 1, 1, 1 );
break;
Wow, wir haben es geschafft! Nun müssen alle drei Teile kompiliert
werden, und der Flammenwerfer sollte funktionieren. Dies war ein sehr langes
Tutorial mit relativ hohem Niveau. Allerdings kann man jetzt basierend auf
diesem seine eigenen Waffen nach belieben einbauen, was den eigenen Mod schon
mal von vielen anderen abheben sollte.
<<
Tutorial 9 | | Tutorial
11
>>
|