Moin!
Ich habe mir gedacht, um LUA einsteigern ein bisschen die Arbeit auf EGM:RP Servern zu zeigen, werde ich eine kleine Erklärung und Anleitung zum Programmieren bei EGM geben.
Hierbei werde ich folgende Punkte erklären
- Unterschiede EGM:RP zu anderen Gamemodes
- Wie erstellt man ein Modul und nutzt es richtig?
- Warum lohnt es sich LUA bei EGM zu lernen?
- Wie fügt man NPC's und Entitys hinzu?
- Performance des Server erhalten
- UI Ansatzweise Responsive gestalten
- Der Vorteil vom Modularen Programmieren
Unterschiede EGM:RP zu anderen Gamemodes
Wer schonmal für DarkRP, TTT oder andere Gamemodes programmiert hat, wird bei EGM:RP eine ziemliche umstellungen bemerken. Mithilfe von EGM:RP ist es möglich, wesentlich weniger Redundanten (Doppelten) Code zu schreiben und viel effizienter zu arbeiten.
Während in den meisten anderen Gamemodes die interne SQLite Datenbank von Garrys Mod oder eine externe MySQL Datenbank verwendet wird, ist das bei EGM:RP etwas anders.
Bei EGM:RP wird zwar eine externe Datenbank verwendet, jedoch ist man nicht darauf angewiesen, diese selbst mit seinem Code anzusprechen.
EGM:RP hat ein einzigartiges “Property” System, welches die gesamte SQL Arbeit für einen übernimmt.
Wenn man Beispielsweise ein Geld-System in EGM:RP implementieren möchte, dann muss man nicht eine SQL Query anhand von Steam-ID usw. schreiben, sondern nutzt dieses Property-System. Man fügt dem Charakter einfach eine Property hinzu, in welcher man Charakter spezifische Daten speichern kann. Das sieht etwa so aus:
Character:AddProperty("money", "number", 0, function(char, value)
if not isnumber(value) then
return false, egmt("shared.invalid_type") --Überprüfen, ob der zu speichernde Wert den Vorraussetzungen entspricht
end
return true
end,
function(char, ply)
return char:BelongsTo(ply)
end)
Alles anzeigen
Hierbei wird dann ein Identifier für diese Property festgelegt.
Danach kommen einige Argumente AddProperty Funktion:
“money” - Der Identifier über welchen wir den Wert abrufen und setzen können
“number” - Der Typ der Property. Kann “number” “string” “table” usw. sein
0 - Der Standardwert, wenn kein anderer gesetzt ist.
Dann gibt es noch 2 Funktionen:
Die erste Funktion Validiert quasi die Eingabe (per Code oder per F6 Menü)
In diesem Beispiel wird nur geschaut, ob der zu speichernde Wert auch wirklich eine Zahl ist.
Die zweite Funktion gibt an, an wen der Wert genetworked werden soll.
Ist eine Property ersteinmal hinzugefügt, dann kann sie überall abgerufen und gesetzt werden:
Das zweite Argument gibt an, welcher Wert genutzt werden soll, sollte money nil sein.
In unserem Fall ebenfalls die 0.
Das setzen ist ebenso einfach:
Mit diesem System spart man sich sowohl das erstellen, von neuen SQL Tabellen sondern auch das Schreiben der SQL Querys, überall wo das Geld gesetzt werden soll.
Wie erstellt man ein Modul und nutzt es richtig?
Eine zweite Besonderheit in EGM:RP ist der Modulare Aufbau.
Jedes Feature liegt in einem einzelnen Modul, welches aktiviert oder deaktiviert werden kann.
Solch ein Modul kann man mit einem selbst geschriebenen Addon vergleichen.
Es kann dabei dann eine oder mehrere Client, Shared und Server Dateien geben.
Normalerweise sieht ein Modul wiefolgt aus:
In der cl_advancedinventory, sh_advancedinventory, sv_advancedinventory ist der jeweilige Code drin.
Die sh_index.lua und die sh_config.lua sind etwas besonders. Die sh_index.lua ist Standardmäßig die einzige Datei in dem Modul, welche bei Aktivierung automatisch ausgeführt wird. Deshalb werden in dieser sh_index.lua die anderen Dateien hinzugefügt.
Das sieht dann in der einfachsten Variante wie folgt aus:
if SERVER then
AddCSLuaFile("sh_config.lua")
AddCSLuaFile("cl_advancedinventory.lua")
AddCSLuaFile("sh_advancedinventory.lua")
include("sh_advancedinventory.lua")
include("sh_config.lua")
include("sv_advancedinventory.lua")
end
if CLIENT then
include("sh_config.lua")
include("cl_advancedinventory.lua")
include("sh_advancedinventory.lua")
end
Alles anzeigen
Hier ist ein Leeres Modul, welches den Aufbau Symbolisiert: https://www.dropbox.com/s/lplqk4v0cpj3…module.rar?dl=0
Warum lohnt es sich LUA bei EGM zu lernen?
EGM:RP nimmt dir viel Arbeit beim Programmieren ab.
Sobald man dieses Property und dieses Modul System verstanden hat, ist man in der Lage mit wenig Programmierkenntnisse viele und relativ große Features zu schreiben.
EGM:RP bietet ebenfalls eine vielzahl von nützlichen Funktionen, welche vor allem für den Anfang zum lernen hilfreich sein können.
Ebenfalls hat man, wenn man es ernst meint mit dem LUA lernen einige direkte Ansprechpartner, welche einem auch bei simpleren Problemen hilfestellung Leisten können (dafür biete ich mich jederzeit an)
Wie fügt man NPC's und Entitys hinzu?
Hier nun noch einmal die Erklärung, wie man denn seine NPC’s und Entitys mit seinen Modulen verknüpfen kann.
Das werde ich etwas mehr am Code zeigen. Wir packen in unser Plain Modul eine Server Side Funktion, welche die HP auffüllt. Das sieht dann wiefolgt aus:
PlainModule = PlainModule or {} --Sicher gehen, dass der PlainModule Table existiert
function PlainModule:RestoreHP(ply)
ply:SetHealth(ply:GetMaxHealth())
ply:ChatPrint(“Du wurdest geheilt.”)
end
Danach muss der NPC im folgenden Ordner erstellt werden:
/garrysmod/gamemodes/hdrrp/entities/entities/healer/
(In meinem Fall ist das Beispiel von HDR:RP)
Im Healer Ordner kommen dann folgende Dateien:
AddCSLuaFile("cl_init.lua")
AddCSLuaFile( "shared.lua" )
include("shared.lua")
function ENT:Initialize()
self:SetUseType( SIMPLE_USE )
self:PhysicsInit( SOLID_VPHYSICS )
self:SetModel("models/trando/npc_cit_sw_trandoshan_bounty_hunter_v1.mdl")
self:CapabilitiesAdd( bit.bor( CAP_ANIMATEDFACE, CAP_TURN_HEAD ) )
self:SetHullType( HULL_HUMAN )
self:SetHullSizeNormal()
self:SetNPCState( NPC_STATE_SCRIPT )
self:SetMovementSequence(2)
self:SetMovementActivity(ACT_IDLE)
self:SetSolid( SOLID_BBOX )
self:DropToFloor()
local phys = self:GetPhysicsObject()
if (phys:IsValid()) then
phys:Wake()
end
end
function ENT:AcceptInput( Name, Activator, Caller )
if Name == "Use" and Caller:IsPlayer() then
PlainModule:RestoreHP(Caller)
end
end
Alles anzeigen
ENT.Type = "ai"
ENT.Base = "base_ai"
ENT.PrintName = "Heiler"
ENT.Category = "EGM:RP"
ENT.Spawnable = true
ENT.AdminSpawnable = true
Über PlainModule:RestoreHP(Caller) in der init.lua (Server-Seitig) rufen wir dann die Methode auf.
Ebenfalls kann hier ein net.Start gemacht werden, um ein UI aus eurer cl_plainmodule.lua aufzurufen.
Performance auf dem Server erhalten
Wenn wir früher oder später möchten, dass unser Server mehr als 10-20 Spieler hat, lohnt es sich, hier und dort über die Performance nachzudenken. Mir geht es hier Hauptsächlich darum, Tick und Think Hooks zu vermeiden.
Die Tick und Think Hook wird jeden Frame auf dem Client und jeden Tick auf dem Server aufgerufen. Das ist je nach Tickrate bis zu 66 mal pro Sekunde auf dem Server und je nach Client bis zu ca. 200 mal pro Sekunde auf dem Client.
Ich habe bisher in meiner gesamten Zeit als LUA Entwickler (inzwischen über 2 Jahre) keinen einzigen Anwendungsfall gehabt, wo ich eine dieser Hooks wirklich brauchte. Es gibt nahezu immer eine Lösung, welche etwas Performanter ist (und wenn es am Ende ein Timer ist, der 1x in der Sekunde auslöst und nicht 200x)
Man darf das auch nicht überdramatisieren, denn nur mit einer einfachen if Abfrage wird man in einer Think hook keinen großen Unterschied merken. Man sollte jedoch wirklich vermeiden, große Code-Blöcke dort auszuführen.
Ein absolutes NO-GO ist, in einer dieser Hooks Networking zu machen (Also einer der :SetNW Funktionien oder ein net.Start) Damit wird euer Server extremst an Performance verlieren.
UI Ansatzweise Responsive gestalten
Hier möchte ich noch einen kleinen Tipp beim erstellen eines UI (User Interface) geben.
Wir haben einige wenige Spieler in EGM, welche nicht mit Full-HD auflösung Spielen (zb. 1280x 1024)
Bei solchen Spielern das UI komplett gleich aussehen zu lassen, habe ich selbst auch noch nicht geschafft, da meine Funktion (die gleich folgt) noch keine Seitenverhältnisse beachtet. Trotzdem werdet ihr mit Nutzung dieser Funktionen solchen Spielern zumindest einen kleinen gefallen tun.
function RWidth(pixel)
return ScrW() / (1920 / pixel)
end
function RHeight(pixel)
return ScrH() / (1080 / pixel)
end
In der Funktion wird davon ausgegangen, dass der PC auf dem das UI getestet wird Full-HD ist. Da ihr in Garrys-Mod Positions und Größenwerte in Pixel angibt, solltet ihr diese Funktionen benutzen, um die Pixel für niedrigere Auflösungen jeweils runter zu rechnen.
Das sieht dann in der Anwendung so aus:
local button = vgui.Create(“EGMButton”)
imageButton:SetPos(RWidth(50), RHeight(100))
imageButton:SetSize(RWidth(150), RHeight(200))
Hier nochmal der Vergleich ohne diese Funktion:
local button = vgui.Create(“EGMButton”)
imageButton:SetPos(50),100)
imageButton:SetSize(150,200)
Für euch wird das dann im ersten Moment nichts verändern, eben nur für Spieler mit niedrigerer Auflösung
Der Vorteil vom Modularen Programmieren
Wer eine Ausbildung zum Programmierer macht, sollte das bereits verinnerlicht haben.
Versuche dein Programm so erweiterbar wie möglich zu machen.
Anfangs ist es oft etwas mehr aufwand, ein Modul erweiterbar zu schreiben, es zahlt sich auf Lange-Zeit jedoch fast immer aus.
Ich gebe mal ein Beispiel, was ich meine:
Am besten lässt sich das mit einem Koch-System veranschaulichen.
Es gibt aktuell 2 Rezepte.
Wenn man es nicht modular macht, könnte das so aussehen:
function CookBread(ply)
if ply:HasIngredient(“flour”) and ply:HasIngredient(“water”) then --HasIngredient ist eine Fiktive Methode
return true -- True zurückgeben aka. das Brot wurde erfolgreich gekocht
else
return false --False Zurückgeben aka. nicht die richtigen zutaten
end
end
function CookSteak(ply)
if ply:HasIngredient(“beef”) and ply:HasIngredient(“water”) then
return true
else
return false
end
end
Alles anzeigen
Man stelle sich nun vor, es gäbe 20 Rezepte und es kommen nach und nach kommen Rezepte dazu. Für jedes Rezept bräuchte man ca. 7 Zeilen Code die man jedes mal selbst eintippen muss.
So würde das ganze Modular aussehen:
Cooking = Cooking or {}
Cooking.Recipes = {
1 = {
name = “steak”,
ingredients = {
1 = “beef”
2 = “pepper”
}
},
2 = {
name = “break”,
ingredients = {
1 = “flour”,
2 = “water”
}
}
}
Alles anzeigen
function CookRecipe(ply, recipe)
local hasAllIngredients = true
for key, ingredient in pairs(recipe.ingredients) do
if not ply:HasIngredient(ingredient) then
hasAllIngredients = false
end
end
if hasAllIngredients then
return true
else
return false
end
end
Alles anzeigen
Das wäre dann die erste Stufe des Modularen Programmierens. Das heißt du hast nicht für jede möglichkeit eine eigene Funktion, sondern eine Config-Datei und du nutzt diese dann in einer einzelnen modularen Funktion.
Eine nächste Stufe wäre noch, diese Config-Datei mithilfe von einer Tool-Gun oder einem Command und einem User-Interface Ingame erweiterbar zu machen.
Damit hätte man sich die gesamte Zukünftige Arbeit, für jedes neue Rezept eine neue Funktion zu schreiben gespart und kann das normale Team diese Aufgaben übernehmen lassen. (Ebenfalls mit deutlich weniger Aufwand.)
Ein weiterer Vorteil ist, dass das Modul auch auf anderen Server verwendet werden kann.