Author | Skamichi |
Last changed | 26.01.2010 22:52 |
Description | Dieses Projekt ist aufgegeben und wird demnächst auf die Freigegeben seite verschoben. Clonk Story ist ein auf Hazard basierendes Projekt dass ich mal vor langer Zeit angefangen habe. Ich wollte etwas machen, dass dem spiel Cave Story identisch ist bzw. ähnelt. Diese Seite dient eher dem vorstellen der technischen Dingen anstatt der spielerischen Details, in der Hoffnung dass jemand vielleicht weitermacht. Story ...gibt es nicht. Aber dafür schonmal ein Konzept für das Storysystem. Wichtige Ereignisse, die Inventar und Umgebung beeinflussen, werden indexiert und aufgelistet. So wird später das laden und speichern von Spielständen leichter. Viele Gegenstände und Umgebungsobjekte werden davon abhängen. global func RestoreAll() { > //Objekte wiederherstellen
> for(var Obj in FindObjects(Find_Func("Restore"))) Obj->Restore();
> //Wir wollens so dunkel haben
> FindObject(DARK)->SetDarkness(Dark);
//Storyobjekte nach Story einstellenfor(var Obj in FindObjects(Find_Func("FollowStory"))) Obj->FollowStory(); > //Lichter erstellen zu anfang einer Sektion
> for(var Obj in FindObjects(Find_Func("IsLamp"))) if(!(Obj->Light())) Obj->CreateLight();
> return(1);
}Wichtige Objekte werden FollowStory() definiert haben und werden es jedes mal beim Laden aufrufen. Was mir hier allerdings noch fehlt ist der Support für mehrere Storyindexe für ein Objekt. Zum Beispiel eine Uhr, die nach und nach immer ein anderes Aussehen hat, je nachdem wo sich die Story gerade befindet. Gegner und Umwelt Bossgegner und wichtige interaktive Objekte werden storyindexiert und werden somit per FollorStory() eingestellt. Wie schon vorher erwähnt fehlt mir die Möglichkeit, ein Objekt auf mehrere Storyindexe einstellen zu können. Gegner und zerstörbare Objekte haben eine Restore() funktion, die jedes mal ausgeführt wird wenn der Spieler den Raum wechselt. (Wobei es für zerstörbare Objekte keinen Template gibt.) /*-- Iceblock --*/ func Restore() { > //Repariert
> Target=1;
> SetGraphics();
SetSolidMask(1,1,18,18,1,1);> return(1);
}/*-- Enemy Template --*/ func Restore() { //Wiederbeleben SetAlive(1); //An Position bringen SetPosition(StartX,StartY); //Und falls was extra muss (bestimmte Action setten) Initialize(); return(1); } Interessant ist es, etwas treffbar zu machen, was eine vollständige SolidMask hat. Bei dem Eisblock musste ich die SolidMask an allen vier Kanten um einen Pixel verringern, damit die Hazard Pistolenkugel merkt, dass sie was getroffen hat. //Oh ja, Touchdamage public func FxTouchTimer() { //Nur wenn draussen! if(!Contained()) { var obj; if(obj=FindObject2(Find_Func("TouchDamage") , Find_AtRect(-8,-10,16,20))) { DoEnergy(-obj->TouchDamage()); DoDamage(1); } } return(1); } Gegner werden normalerweise dem Spieler schaden, wenn sie ihn berühren. Sie haben dafür eine Funktion, die den Schadenswert liefert. Der Clonk selbst hat einen Timer, der nachschaut, ob er einen gegner berührt. So kann man theoretisch auch einen Gegner bauen, der den Clonk bei berührung heilt. Wer weiß. Ein Gegner wird natürlich nicht als gefunden gezählt, wenn TouchDamage() 0 zurück liefert. Gegenstände Aufsammelbare Gegenstände wie Erfahrung oder Leben sind ziemlich simpel und haben kein Template. Sie existieren auf der Karte als konkrete Objekte und verschwinden bei Aufnahme. Größe skaliert mit steigenden Wert der Erfahrung oder Leben. Raketen sollten 2 fixierte Werte haben. Waffen und ähnliche Storybasierende aufsammelbare Gegenstände sind schon etwas komplizierter. Da ich die Kontrolle über das variierende Spielgeschehen mit dem Laden/Speichern System noch halbwegs beibehalten will, habe ich jeweils einen Platzhalter und eine richtige Instanz für Waffen oder andere benutzbare Gegenstände. Die Platzhalter sind im Items Ordner mit dem Prefix "Fake". Items, die einen Platzhalter haben, aber, wenn aufgesammelt, keinen direkten Input benötigen, wie zum Beispiel der Energiekonverter, haben den prefix nicht und auch keinen echten Bruder. Der Platzhalter übergibt dem Spieler die echte Instanz oder fügt dem Spieler den Itemindex hinzu und deaktiviert sich. Falls der Spieler aber danach stirbt ohne vorher gespeichert zu haben, kann ich ihm einfach den Gegenstand wieder wegnehmen oder den entsprechenden Indexeintrag löschen und den Platzhalter wieder aktivieren. public func FollowStory() { //Ist es storybasiert? if(GetStoryIndex()) { > //Story schon zu weit?
> if(GetStory() >= GetStoryIndex()) {
> //Dann mal verschwinden lassen
> Collected=1;
> SetClrModulation(RGBa(0,0,0,255));
> return(1);
> }
> else {
> //Ansonsten noch zu kriegen
> Collected=0;
> SetClrModulation(RGB(255,255,255));
> return(1);
> }
> }
//Nun gut, dann einfach nur fragen ob wir es schon haben if(GetItemIndex()) { > if(GetCrew()->GetItem(GetItemIndex())) {
> //Dann mal verschwinden lassen
> Collected=1;
> SetClrModulation(RGBa(0,0,0,255));
> }
> else {
> //Ansonsten noch zu kriegen
> Collected=0;
> SetClrModulation(RGB(255,255,255));
> }
> }
> return(1);
}Wichtige Items werden der Reihe nach auch indexiert und können somit leicht aktiviert oder deaktiviert werden, je nachdem in welchem Storyindex sich der spieler gerade befindet. Weniger wichtigere, aber einmalige Items, wie zusätzliche Ausrüstung oder Health- und Misslecontainers, werden aktiviert und deaktiviert, je nachdem ob sie schon im Inventar des Spielers sind oder nicht. Zu erwähnen sei, dass der Spieler sein Inventar nicht ablegen kann. Das Spielgeschehen und die Platzierungen/Umtauschungen von Waffen sollte so balanziert werden, so dass der Spieler so wie in Cave Story nicht zu viele Waffen gleichzeitig trägt. Waffen Genauso wie in Cave Story sollte die Auswahl an Waffen sehr exotisch sein; man hat schon an einem Schild gedacht. Hier liegt ein großer Schwerpunkt bei der umschreibung der Hazardwaffen. public func DoWpnEnergy(int iEner) { > //Kurz eine Sicherheitskopie von alten Ener und Aenderung
> var OldEner=GetAmmo(GetFMData(FM_AmmoID));
> var ChangeEner=iEner;
//Erstmal Energie in die Waffe stopfen (egal ob positiv oder negativ) DoAmmo(GetFMData(FM_AmmoID),BoundBy(ChangeEner,-GetAmmo(GetFMData(FM_AmmoID)),GetFMData(FM_AmmoLoad)-GetAmmo(GetFMData(FM_AmmoID)))); //Dann gucken ob voll ist if(GetAmmo(GetFMData(FM_AmmoID)) == GetFMData(FM_AmmoLoad)) //Und ob der ueberhaupt noch leveln kann if(GetFireMode() < MaxLevel()) { //Yay, leeren DoAmmo(GetFMData(FM_AmmoID),-GetAmmo(GetFMData(FM_AmmoID))); //LvlUp! ++firemode; > Message("Level Up!|{{OMFG}}",this());
> }
> //Wenn aber zu viel Schaden
> if(ChangeEner+OldEner < 0)
> //Und wenn der noch runter kann
> if(GetFireMode() > 1) {
> //Mathematik Teil 1: Leeren
> DoAmmo(GetFMData(FM_AmmoID),-GetAmmo(GetFMData(FM_AmmoID)));
> //LvlDown!
> --firemode;
> Message("<c ff0000>Level Down!|{{OMFG}}",this());
> //Mathematik Teil 2: Fuellen
> DoAmmo(GetFMData(FM_AmmoID),GetFMData(FM_AmmoLoad));
> //Mathematik Teil 3: Den noch ueberschuessigen Schaden abziehen,
> //dabei beachten dass man nicht mehr als einen Lvl gleichzeitig fallen kann
> DoAmmo(GetFMData(FM_AmmoID),BoundBy(ChangeEner+OldEner,-GetAmmo(GetFMData(FM_AmmoID)),GetFMData(FM_AmmoLoad)-GetAmmo(GetFMData(FM_AmmoID))));
> }
> return(1);
}Ihr könnt es euch vielleicht schon denken. Ich benutze hier die Munition der Waffe als Erfahrungsbalken und die Schussmodi als Level. Das bedeutet, dass ich eine globale Munitionsquelle für alle Waffen des gleichen Munitionstyp brauche. public func Fire1() { > //Wenn schon 2 schuesse da sind, warte mal
> if(Bull1 && Bull2) return();
> var user = GetUser();
//Muni da?if(user->GetAmmo(GetFMData(FM_AmmoID))==0) return(); user->DoAmmo(GetFMData(FM_AmmoID),-1); > var angle = user->AimAngle(20) + RandomX(-1,+1);
> var x,y; user->WeaponEnd(x,y);
> var ammo = CreateObject(SHT1,x,y,GetController(user));
> ammo->Launch(angle,60,70,5,10,GetFMData(FM_Damage, 1));
> // Effekte
> MuzzleFlash(40,user,x,y,angle,RGB(233,255,255));
> // Sound
> Sound("Pistolshot.ogg",0,ammo);
> //Und mal registrieren
> if(!Bull1) Bull1 = ammo;
> else(Bull2 = ammo);
}Dafür benutze ich die Munition aus dem Munitionsgürtel des Hazardclonks. Der Clonk hat zur Zeit einen Timer, der ihm Energie generiert, das könnte man eventuell umschreiben, so dass andere Items Einfluss darauf haben. Zum Beispiel ein mächtiger Item, der im Gegenzug so viel Energie abzieht, so dass die Munitionsgenerierung gleich null ist. Außerdem wird keine Energie erzeugt solange autoonfire Waffen gefeuert werden. Speichersystem Das Speichersystem war eine spontane Idee, hat sich aber bis hierhin gut gehalten. Ich wollte nur Objekte und Ähnliches abhängig machen, so dass ich keine Änderungen an der Landscape machen muss und ich alles im Spieler speichern kann, falls mal Coop oder Versus möglich sein sollte. /*-- Save game / Load game --*/ //Integer = 2.147.483.647 //CS_Item: Binaersystem, jeder 2er Potenz wird ein Item zugeordnet. Geht nur solange innerhalb integer. //CS_HealthCon: Binaersystem, jeder 2er Potenz wird ein HealthContainer zugeordnet. //CS_MissleCon: Binaersystem, jeder 2er Potenz wird ein MissleContainer zugeordnet. //CS_Stat: 0000(1) 000(2) 000(3). (1) = Storyindex. (2) = MaxHP. (3) HP. //CS_Posi: 00(1) 0000(2) 0000(3). (1) = Section. (2) = Y Position. (3) = X Position. //CS_Wpn*: 000(1) 0000(2) 000(3). (1) = Level. (2) = Ammo. (3) = Experience. Imaginäre Items und Containers werden irgendwo im Clonk einer 2er Potenz zugewiesen, so dass man eindeutig bestimmen kann, welche Objekte beim Laden aktiviert und deaktiviert werden müssen. Die anderen Variablen werden eingeteilt, allerdings bin ich mir hier nicht sicher, ob Munition hier Sinn macht, da mehrere Waffen gleiche Munitionstypen verwenden werden. global func LoadGame(iPlr) { var Item = GetPlrExtraData(iPlr,"CS_Item"); //Items rausnehmen und in Clonk stopfen for(var i = 30;i >= 0;i--) { if(Item - 2**i >= 0) { Item -= 2**i; GetCrew(iPlr)->AddItem(i,1); } //Ansonsten Items wegnehmen die er nicht haben sollte else ( GetCrew(iPlr)->AddItem(i,0) ); } An den Grenzen des Integers gehalten, können ungefähr 30 verschiedene Items per variable gespeichert werden. Einfach gehalten mit einem Object-Call an den Clonk mit AddItem(). var Stat = GetPlrExtraData(iPlr,"CS_Stat"); //Halbtot DoEnergy(-GetEnergy(GetCrew(iPlr))+1,GetCrew(iPlr)); //Physical stellen SetPhysical("Energy",((Stat%1000000)/1000)*1000,2,GetCrew(iPlr)); //Und wieder einfuegen DoEnergy((Stat%1000)-1,GetCrew(iPlr)); LocalN("OldHP",GetCrew(iPlr))=GetEnergy(GetCrew(iPlr)); //Story folgen SetStory(Stat/1000000); SetAction("Walk",GetCrew(iPlr)); Hier muss man nur aufpassen, dass man sich nicht bei den 10er Potenzen vertut. global func LoadWeapon(AiDie, iPlr, Weapon) { var Id; if(AiDie==1) Id=PIST; var obj; if(!(obj=FindObject2(Find_Func("WeaponID",AiDie)))) obj=CreateContents(Id,GetCrew(iPlr)); obj->LocalN("firemode")=1; obj->DoWpnEnergy(-999); obj->LocalN("firemode")=Weapon/10000000; obj->DoWpnEnergy(Weapon%1000); GetCrew(iPlr)->DoAmmo(obj->GetFMData(FM_AmmoID),(Weapon%10000000)/1000); return(1); } Wie gesagt, mir ist nicht klar ob es wirklich sinnvoll ist, die Munition pro Waffe zu Speichern. Auch hier aufpassen, dass die Variablen nicht die Grenzen überschreiten. Beim Laden der Position des Clonks wird es eventuell zur Sektionswechsel kommen. Ich habe keine Ahnung wie das funktioniert, also ist es noch nicht eingebaut. Spieler Ein anderer langwieriger Teil ist die Ergänzung des Hazardclonks. protected func Initialize() { AddEffect("Move",this(),20,1,this()); AddEffect("RechargeAmmo",this(),20,20,this()); AddEffect("Touch",this(),20,1,this()); return(_inherited()); } Wie bereits erwähnt, werden Energiegenerierung und Touchdamage mit Timer gesteuert. Aber auch eine billige Ergänzung zur Steuerung. public func FxMoveTimer() { if(!(GetAction() S= "Jump" || GetAction() S= "JumpArmed")) { return(1); } if(GetContact(0, -1) & CNAT_Left || GetContact(0, -1) & CNAT_Right) return(0); if(Move) SetXDir(BoundBy(GetXDir()+Move,-Abs(Move)*10,Abs(Move)*10)); else(SetXDir(BoundBy(GetXDir()-BoundBy(GetXDir(),-2,2),-20,20))); return(1); } Der Timer setzt die Geschwindigkeit des Clonks in der Luft anhand von der Variable Move. Move wird in diesen Fall jedes mal Gesetzt, wenn der Spieler Richtungstasten drückt. So habe ich erstmal eine Lösung für die noch etwas doofe JnR steuerung. Geplant war auch ein Jet-Pack wie in Cave Story, aber bisher kam mir noch keine Idee, wie ich das umsetzen könnte. protected //Zum Einfuegen und wegnehmen von imaginaeren Items public func AddItem(int iNumber, int iAdd) { //Wir wollen ja innerhalb des Integers bleiben if(iNumber <= 30 && iNumber >= 0) PlrItems[iNumber]=iAdd; return(1); } public func AddHealthCon(int iNumber, int iAdd) { //Wir wollen ja innerhalb des Integers bleiben if(iNumber <= 30 && iNumber >= 0) HealthCons[iNumber]=iAdd; return(1); } public func AddMissleCon(int iNumber, int iAdd) { //Wir wollen ja innerhalb des Integers bleiben if(iNumber <= 30 && iNumber >= 0) MissleCons[iNumber]=iAdd; return(1); } Ziemlich simpel; alle wichtige und nutzlose Items werden einfach in Arrays gespeichert. Einfacher Zugriff, einfache Abfrage. Was mir noch nicht klar ist, ist wie ich den Game Over hinbaue, ich fing damit an, den Clonk erst gar nicht sterben zu lassen. //Lassen wir den erst gar nicht sterben protected func Death() { SetAlive(1); SetAction("Walk",0,0,true); MakeCrewMember(this(),GetOwner()); SetCursor(GetOwner(),this()); //Effekte wieder aufbauen, die beim Sterben weggegangen sind wpneffect = AddEffect("ShowWeapon",this(),1,1,this(),HZCK); AddEffect("DmgCheck",this(),1,0); //Scheintot Sound("Die"); return(1); } So könnte man zum Beispiel den Clonk sofort in einen unsichtbaren Container packen und den Game Over Screen einblenden. Nun, viel mehr fällt mir nicht ein. Appearently I added English descriptions and string tables into the objects, so I was probably making it in English too. You have to figure out the script yourself, though. http://www.ccan.de/cgi-bin/ccan/ccan-view.pl?a=view&i=5621 http://www.miraigamer.net/cavestory/info_1.php |
Completion | Planning |
Date of publication | Damals |
User niveau | Average |
Last change: 01.01.2011 23:20
Genau ;D
Siehe
http://www.ccan.de/cgi-bin/ccan/ccan-view.pl?a=view&i=5621--> Autor