PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Tutorial|Tipps - Programmieren mit Funktionen



Boïndil
05.06.2011, 15:31
Hallo, Leute,
mich hat es grausam genervt, wie aufgebläht und ineffizient die Scripte in Vanilla sind, was bedeutet, dass sie auch schwer zu warten und zu debuggen sind.
Man denke da nur an die diversen ellenlangen Warten-Befehle vor jedem DLC, die teilweise in Quest-Scripten, aber teilweise auch Result-Scripts sind.
Einige Modder haben teilweise eine bessere Programmier-Moral als die Original-Entwickler, arbeiten teilweise schon mit Funktionen, aber zu wenig weit.
Liegt vielleicht daran, dass ich frisch in die Sprache eingestiegen bin, aber mir sonst gewöhnt bin, mit JS, VB und dem unglaublich skalierbaren, effizienten und leistungsfähigen C#.Net zu programmieren. Hier gibt es keine Notwendigkeit, Code haufenweise zu duplizieren und es ist unglaublich, wie kurz Scripte sein können, die ein komplettes Gebiet abdecken. Klassen, Vererbung und Überladungen kann ich nicht nachrüsten, aber zumindest das beste aus dem wenigen rausholen.

Also habe ich begonnen, ein System zu entwicklen, wo Funktionen abgekapselt und flexibel einzusetzen sind, dadurch sind sie übersichtlicher und einfacher zu warten und dies vor allem an einem Ort.

Dies beginnt als Beispiel mit einer neuen Quest MeinModBegleiterFunktionenQuest.
Diese braucht wahrscheinlich keine hohe Priorität, aber die Script-Processing-Delay setzen wir auf Default, kann aber auch höher sein, falls es nicht um Speed geht. Und natürlich muss sie Start Game Enabled sein.

Generell geht es darum, dass Scripte überall im Spiel Funktionen mit Variabeln aus der Funktionen-Quest aufrufen können.
Die Entscheidung, die Funktion aufzurufen, kann in einem Actor-Effect-Script, einem Aktivator, einem Quest-Script oder einem Itemscript sein. Hier muss darauf geachtet werden, dass die Konditionen stimmen und die Parameter korrekte Werte kriegen.

Die Funktionen selbst führen nur die Variabeln aus, sie überprüfen nicht selbst, ob die Kondition stimmt und die Variable einen korrekten Wert hat.
Man könnte das natürlich tun, bläht dann aber die Scripte wieder auf.

Bevor man beginnt, sollte man sich überlegen, welche Tasks zu einem bestimmten Bereich gehören und welche vor allem oft benötigt werden:
-Warten
-Folgen
-Heilen
-Wiederbeleben
Gerade bei den Begleitern muss man einberechnen, dass Tasks entweder nur einen oder alle betreffen können. So bricht man die Funktionen auf die kleinsten möglichen Teile runter, die einzeln für sich überschaubar sind.
Manche Tasks sind oberflächlich gesehen eine Funktion, können aber aus mehreren Sub-Funktionen bestehen, die auch Teil von anderen Funktionen sind.
Z.B. bedeutet ein Feuern, dass der Kampfstil auf Default gesetzt wird, dass dem Follower alle Verbindungen zum Player wie Fraktion und TeamMate gekappt werden werden und dass er zu seinem HomeMarker gebeamt wird. Also machen wir keine einzelne Funktion "Feuern" sondern die Funktionen "Kampfstil", "Als Begleiter deaktivieren" und "Gehe zum HomeMarker". Denn das sind alles einzelne Funktionen, die auch anderweitig benutzt werden.

Anschliessend muss man sich überlegen, welche Unterfunktionen in die Funktion gehören und welche in den Aufruf.
Beispiel: Die Funktion Warten benötigt kein Wissen darüber, in welchem Gebiet sie sich befindet und welche Message z.B. beim Warten ausgelöst wird, denn sonst würde man die Funktion wieder mit haufenweisen GetInWorldSpace und einer Auflistung von passenden Messages aufblähen.
Hingegen weiss der Aufrufer(z.B. BogDoorScript), wo er sich befindet, denn er löst ja das Warten aus, weil man z.B. die Bog-Door öffnet.

Bei der Funktion muss man sich auch überlegen, ob eine Steuerung dazu gehört. Wenn man zum Beispiel eine Funktion "Waffen eines Begleiter reparieren" erstellt, benötigt es folgende Angaben
-Reparaturhöhe als Float
-Begleiter-Index (0 = alle, 7 = Cross)

In den folgenden Beispielen werde ich die eigentlichen Funktionen auf das wesentliche begrenzen, jeder weiss ja, wieviel Zeilen Code es bedeutet, einen Begleiter professionell zu feuern.

Also beginnt unser neues Quest-Script:



scn MeinModBegleiterFunktionenScript

short TaskType ; Short: Index der Funktion
short TaskRef ; Short: Interner Index des Object
short TaskShort1 ; Short: Generischer Wert als Short
short TaskShort2 ; Short: Generischer Wert als Short
short TaskShort3 ; Short: Generischer Wert als Short
short TaskShort4 ; Short: Generischer Wert als Short
float TaskFloat1 ; Float: Generischer Wert als Float
float TaskFloat2 ; Float: Generischer Wert als Float
float TaskFloat3 ; Float: Generischer Wert als Float
float TaskFloat4 ; Float: Generischer Wert als Float
float TaskDelay ; Float: Gegebenenfalls Verzoegerung der Funktion

Begin GameMode

If TaskDelay <= 0

; Begleiter feuern
; TaskRef: 0 = Alle, FollowerIndex = einzelner Begleiter
If TaskType == 1

If TaskRef == 0 || TaskRef == 1
Set Followers.ButchHired To 0
Set Followers.ButchFired To 1
ElseIf TaskRef == 0 || TaskRef == 2
Set Followers.CrossHired To 0
Set Followers.CrossFired To 1
ElseIf TaskRef == 0 || TaskRef == 3
Set Followers.CloverHired To 0
Set Followers.CloverFired To 1
EndIf

; Variablen-Reset (Pflicht), Weiterleitung aber moeglich
Set TaskRef To 0
Set TaskType To 0

; Begleiter wiederbeleben
; TaskRef: 0 = Alle, FollowerIndex = einzelner Begleiter
ElseIf TaskType == 2

If ButchRef.GetDead == 0 && (TaskRef == 0 || TaskRef == 1 )
ButchRef.Ressurect
ElseIf CrossRef.GetDead == 0 && (TaskRef == 0 || TaskRef == 2 )
CrossRef.Ressurect
ElseIf CloverRef.GetDead == 0 && (TaskRef == 0 || TaskRef == 3 )
CloverRef.Ressurect
EndIf

; Variablen-Reset (Pflicht), Weiterleitung aber moeglich
Set TaskType To 3 ; Gesundheit erhoehen
Set TaskDelay To 5 ; Delay setzen

; Begleiter Gesundheit Reset
; TaskRef: 0 = Alle, FollowerIndex = einzelner Begleiter
ElseIf TaskType == 3

If TaskRef == 0 || TaskRef == 1
ButchRef.ResetHealth
ElseIf TaskRef == 0 || TaskRef == 2
CrossRef.ResetHealth
ElseIf TaskRef == 0 || TaskRef == 3
CloverRef.ResetHealth
EndIf

; Variablen-Reset (Pflicht), Weiterleitung aber moeglich
Set TaskRef To 0
Set TaskType To 0

; Begleiter Waffenstil Ranged
; TaskRef: 0 = Alle, FollowerIndex = einzelner Begleiter
ElseIf TaskType == 4

; Funktionen

; Variablen-Reset (Pflicht), Weiterleitung aber moeglich
Set TaskRef To 0
Set TaskType To 0

; Begleiter Waffenstil Melee
; TaskRef: 0 = Alle, FollowerIndex = einzelner Begleiter
ElseIf TaskType == 5

; Funktionen

; Variablen-Reset (Pflicht), Weiterleitung aber moeglich
Set TaskRef To 0
Set TaskType To 0

EndIf

Else
Set TaskDelay To TaskDelay - GetSecondsPassed
EndIf

End


Erläuterung

TaskType: Ist die Index-Nummer der Funktion
TaskRef: Ist die Index-Nummer des aktuellen Objects(Waffen, Outfits, NPC)
TaskDelay: Macht es möglich, die Funktion verzögert ablaufen zu lassen
TaskShortX, TaskFloatX: sind mögliche weitere Parameter mit diesen Daten-Typen( Stat-Modifier, HealthPercent, Scale)


ScriptAblauf:
Im Actor-Script wird entschieden, dass die Waffe eines speziellen Begleiters repariert werden muss.
Also setzt man den TaskType auf 9(Waffen reparieren), den TaskRef auf Clover(3), und nimmt den TaskFloat1, um den Wert der Reparatur zu bestimmen.

; Waffen eines Begleiters reparieren

If GetWeaponHealthPercentage < 0.85
; Clovers Waffe reparieren
Set MeinModBegleiterFunktionenQuest.TaskRef To 3
Set MeinModBegleiterFunktionenQuest.TaskFloat1 To 0.85
Set MeinModBegleiterFunktionenQuest.TaskType To 9
EndIf

Weitere Beispiele:

; Begleiter-Grösse als Spielerei beim Betreten einer Zelle

If DoOnce == 0 && Player.GetInCell MeinModCellX == 1
; Clover vergroessern
Set MeinModBegleiterFunktionenQuest.TaskRef To 3
Set MeinModBegleiterFunktionenQuest.TaskFloat1 To 1.8
Set MeinModBegleiterFunktionenQuest.TaskType To 7
EndIf


; Begleiter nach einem Gefecht automatisch heilen oder wiederbeleben

Begin OnCombatEnd
If GlobalFollowerHealMode == 1
; Alle Begleiter wiederbeleben und auf vollstaendige Gesundheit bringen
Set MeinModBegleiterFunktionenQuest.TaskType To 2
ElseIf GlobalFollowerHealMode == 2
; Alle auf vollstaendige Gesundheit bringen
Set MeinModBegleiterFunktionenQuest.TaskType To 3
EndIf
End


Wie man im letzten Beispiel (auch im Script)sieht, ist es auch möglich Funktionen durch interne Sprünge anzusteuern.
Bei TaskType 2 wird der Begleiter ressurected, dann ein Delay gesetzt und die Gesundheit des Begleiter mit einer Weiterleitung auf TaskType 3 wiederhergestellt.
Bei TaskType 3 wird nur die Gesundheit wiederhergestellt.
Also bedeutet 2 (= Wiederbeleben) auch 3 (= Gesundheit reseten).

Hier noch ein aktuelles Beispiel vom Code für ein Warten von definierten Begleitern(oder allen) an einem bestimmten Ort oder gleich hier.
Die HomeMarker für alle oder einzelne Begleiter oder einem bestimmten CurrentHomeMarker wird in den betreffenden Scripten bestimmt.

Z.B. hat die Bog Door im InActivate mein Script:

scn DLC04BogDoorSCRIPT
; Followers have to wait.
; wlwFollowersNotAllowed wird aufgehoben in Quest DLC04MQ02, Stage 180
Begin OnActivate Player
if GetStage DLC04MQ02 == 150
setStage DLC04MQ02 180
else
If Followers.PlayerHasFollower == 1
wlwFollowersCurrentWaitMarker.MoveTo DLC04BogAwakenMarker
ShowMessage wlwDLC04BogFollowersMsg
Set wlwFollowersNotAllowed To 1
Set Followers.TaskShort1 To 3
Set Followers.TaskType To 3
EndIf
; activate player (thanx @ Aaaaaimbot)
Activate
endif
End

Hier der aktuelle Warten-Block im Follower-Quest-Script:


; Param TaskType: 3 = FollowersWait
; Param TaskRef : 0 = All | FollowerId
; TaskShort1: 0 = Aktuelle Position | 1 == wlwFollowersXXXHomeMarker | 2 = wlwFollowersHomeWaitMarker | 3 = wlwFollowersCurrentWaitMarker
; TaskShort2: 0 = FollowerWaitTime | Integer = Integer * FollowerWaitTime
If TaskType == 3

If TaskShort2 == 0
Set TaskShort2 To FollowerWaitTime
Else
Set TaskShort2 To (FollowerWaitTime * TaskShort2)
EndIf

If Followers.DogmeatHired == 1 && DogmeatRef.Waiting == 0 && (TaskRef == 0 || TaskRef == 1)
Set DogmeatRef.Waiting To 1
Set Followers.DogmeatWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
DogmeatRef.MoveTo wlwFollowersDogMeatHomeMarker
ElseIf TaskShort1 == 2
DogmeatRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
DogmeatRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.ButchHired == 1 && ButchRef.Waiting == 0 && (TaskRef == 0 || TaskRef == 2)
Set ButchRef.Waiting To 1
Set Followers.ButchWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
ButchRef.MoveTo wlwFollowersButchHomeMarker
ElseIf TaskShort1 == 2
ButchRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
ButchRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.CharonHired == 1 && CharonRef.Waiting == 0 && (TaskRef == 0 || TaskRef == 3)
Set CharonRef.Waiting To 1
Set Followers.CharonWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
CharonRef.MoveTo wlwFollowersCharonHomeMarker
ElseIf TaskShort1 == 2
CharonRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
CharonRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.CloverHired == 1 && CloverRef.Waiting == 0 && (TaskRef == 0 || TaskRef == 4)
Set CloverRef.Waiting To 1
Set Followers.CloverWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
CloverRef.MoveTo wlwFollowersCloverHomeMarker
ElseIf TaskShort1 == 2
CloverRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
CloverRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.JerichoHired == 1 && JerichoRef.Waiting == 0 && (TaskRef == 0 || TaskRef == 5)
Set JerichoRef.Waiting To 1
Set Followers.JerichoWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
JerichoRef.MoveTo wlwFollowersJerichoHomeMarker
ElseIf TaskShort1 == 2
JerichoRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
JerichoRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.FawkesHired == 1 && MQ08FawkesRef.Waiting == 0&& (TaskRef == 0 || TaskRef == 6)
Set MQ08FawkesRef.Waiting To 1
Set Followers.FawkesWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
MQ08FawkesRef.MoveTo wlwFollowersFawkesHomeMarker
ElseIf TaskShort1 == 2
MQ08FawkesRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
MQ08FawkesRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.RL3Hired == 1 && RL3Ref.Waiting == 0 && (TaskRef == 0 || TaskRef == 7)
Set RL3Ref.Waiting To 1
Set Followers.RL3WaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
RL3Ref.MoveTo wlwFollowersRL3HomeMarker
ElseIf TaskShort1 == 2
RL3Ref.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
RL3Ref.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf
If Followers.StarPaladinCrossHired == 1 && StarPaladinCrossRef.Waiting == 0 && (TaskRef == 0 || TaskRef == 8)
Set StarPaladinCrossRef.Waiting To 1
Set Followers.CrossWaitingLeaveDay To( GameDaysPassed + TaskShort2 )
If TaskShort1 == 1
StarPaladinCrossRef.MoveTo wlwFollowersCrossHomeMarker
ElseIf TaskShort1 == 2
StarPaladinCrossRef.MoveTo wlwFollowersHomeWaitMarker
ElseIf TaskShort1 == 3
StarPaladinCrossRef.MoveTo wlwFollowersCurrentWaitMarker
EndIf
EndIf

Set TaskType To 0
Set TaskRef To 0
Set TaskShort1 To 0
Set TaskShort2 To 0

EndIf

Ich hoffe, dass das einigermassen verständlich war und dass ich damit einige Anregungen für ein effizientes Coden geben kann und stehe natürlich auch für eine Diskussion bereit.
Diese Methoden sind natürlich nicht auf Begleiter begrenzt.

Grüsse und ein schönes Weekend
Boïndil (der gerade wieder in Pitt angekommen ist)

Aaaaaimbot
07.06.2011, 10:10
Mach doch endlich mal den Bug mit der Bogdoor richtig weg. Der Bug resultiert nämlich daraus, daß da "Onactivate Player" steht, was nicht richtig funktioniert (den Bug haben die DLC-Ersteller eingebaut). Der Compiler schluckt es zwar, verhalten tut es sich aber wie ein "OnActivate" ohne Player. Wie schon gesagt: Wenn man den Bug richtig wegmacht (was ich getan hab), ist es gar nicht nötig, die Begleiter vornedran warten zu lassen! :P

Boïndil
07.06.2011, 10:20
Den werde ich wegnehmen, habe mich auch schon gefragt, wieso Activate Player steht, wenn nur der Player überhaupt aktivieren kann.
Halte es allerdings für passend, dass man da ohne Begleiter rein geht.

Was meinst du zu diesem Script-Konzept?

Aaaaaimbot
07.06.2011, 10:29
Den werde ich wegnehmen, habe mich auch schon gefragt, wieso Activate Player steht, wenn nur der Player überhaupt aktivieren kann.

Wie "nur der Player aktivieren kann"? Wenn der Begleiter versucht reinzugehen, aktiviert er die Tür ja auch, und genau dann gibts nämlich den Bug, daß der SPIELER an den Anfang zurückteleportiert wird, weil "activate player" bedeutet, daß die Tür für den SPIELER aktiviert wird und es also so ist, als wär der Spieler durch die Tür gegangen und nicht etwa der Begleiter! Man muß also, wenn man den Bug fixed, auf jeden Fall auch das "activate player" durch "activate" ersetzen.


Was meinst du zu diesem Script-Konzept?

Für nen Lehrling ganz gut :D, allerdings nützt mir das natürlich nicht so viel, weil ich weiß eh so ziemlich alles über das Skripting in Fallout :P. Wobei, neue Modder profitieren vielleicht von so einer Anleitung, obwohl vermutlich jeder seinen eigenen Modder-Stil hat.

Boïndil
07.06.2011, 11:03
Das Onactivate Player bleibt natürlich, aber das Activate Player muss nur durch Activate ersetzt werden. Schon verstanden.

Hier ging es ja nicht um den Wortschatz und die Auswirkungen auf das Spiel, sondern nur um das effiziente Schreiben von Code.

Aaaaaimbot
07.06.2011, 11:07
Das Onactivate Player bleibt natürlich, aber das Activate Player muss nur durch Activate ersetzt werden. Schon verstanden.

Vorsicht: Sowas kann zu Folge-Bugs führen, wenn man sich dann das Skript anschaut und instinktiv denkt "ah ja, ONACTIVATE PLAYER kann funktionieren" und das in nem anderen Skript wieder verwendet und sich erst mal wundert, warum es dauernd rumbuggt. Um sicherzugehen, daß Du nicht irgendwo anders nen "ONACTIVATE PLAYER"-Block einbaust in der Hoffnung, daß der auch wirklich nur vom Player aktiviert wird, würd ich es in ein simples "OnActivate" umschreiben.

Bist Du nicht eigentlich der, der immer komplett saubere Skripte haben will? :P

Boïndil
07.06.2011, 11:13
Inwischen habe ich schon mitgekriegt, wann und wo nur der Player aktivieren darf und ausserdem versuche ich auf Instinkt beim Coden zu verzichten ;)

Boïndil
11.06.2011, 08:06
Muss es jetzt schon sagen, dass mein System ein voller Erfolg ist, wie ich an meinen Belgleiter-Funktionen merke.

Bin gerade dabei, noch die letzten Vanilla-Quests und Scripte anzupassen, wo es um das Folgen/Warten von Begleitern geht, die ja jetzt alle eigene FollowerWaitime haben, z.B FollowerWaitTimeCross. Das beinhaltet Zeta, die Tranquility-Lane, Anchorage und weitere.

Ich lösche also die 50 Zeilen Warten-Befehle aus Vanilla und füge zusätzlich meine Automatik hinzu, die Begleiter auf automatisches Folgen stellt, sobald sie wieder dürfen. Das hat nämlich keiner gemacht, weil es eben um 50 Zeilen Code geht.

Also statt:

If Followers.ButchHired == 1 && ButchRef.Waiting == 1
Set ButchRef.Waiting To 0
Set Followers. ...
Set Ellenlang To 10
If Followers.CrossHired == 1 && CrossRef.Waiting == 1
Set CrossRef.Waiting To 0
Set Followers. ....
Set Ellenlang To 10
Set Ellenlang To 127

Nur noch:

Set FollowerQuest.TaskType To 4

Kommt hinzu, dass dieser Befehl auch automatisch für die geplanten weiteren X Begleiter gilt.
Wenn das mal kein Vorteil ist...

Hätte ich von Beginn weg so gearbeitet, hätte ich mir nochmals mehrere hundert Zeilen Code erspart.