Byty, istotne zdarzenia i studium pluginu
Scripting AMXX
Cel
- Nauka o istocie bytów
- Przedstawienie istotnych zdarzeń
- Tworzenie optymalnych pluginów
- Znajomość podstawowych metod komunikacji gracza z serwerem
- Informacje wstępne
Spoiler - Zrozumienie istoty działania AMXX
- Znajomość podstawowych funkcji
- Umiejętność importowania bibliotek
- Zwracanie odpowiedniej wartości funkcji
- Odczytywanie argumentów wiadomości
Poza poznanymi już funkcjami związanymi z samym pluginem,
istnieją także forwardy, wywoływane na podstawie czynności gracza.
Przykładowe funkcje to:
- client_connect wywoływana w momencie rozpoczęcia łączenia się gracza z serwerem
- client_authorized wywoływana w momencie uzyskania przez gracza flag uprawnień
- client_putinserver wywoływana w momencie pobrania wszelkich wymaganych plików i wejścia do gry
- client_disconnect wywoływana w momencie wyjścia gracza z serwera
- client_infochanged wywoływana w momencie zmiany informacji klienta, np. nicku
- client_command wywoływana w momencie wpisania jakiejkolwiek komendy do konsoli
Uwaga! Podczas, gdy client_connect jest pierwszą czynnością dokonywaną w momencie łączenia się gracza z serwerem,
nie można ustalić kolejności wykonywania funkcji client_authorized oraz client_putinserver, mogą być one wykonane w różnej kolejności.
Dlatego zaleca się dla niektórych instrukcji, sprawdzenie wywołania obydwu tych funkcji poprzez utworzenie prostej funkcji client_ingame
Warto znać parę tych podstawowych forwardów z biblioteki amxmodx, gdyż często się przydają.
client_cmd
Serwer może wykonać pewne komendy u klienta tak, jakby on sam je wykonał.
Służy ku temu funkcja client_cmd, która za pierwszy argument
przyjmuje numer identyfikacyjny gracza, a za drugi komendę do wykonania u klienta.
Pozostałe opcjonalne argumenty uzupełniają zmienne w ciągu polecenia z 2. argumentu.
Przykład zastosowania. Funkcja ustawia graczowi interp na 0.01 w momencie pełnego połączenia z serwerem:
public client_ingame(){
new Float:interp=0.01;
client_cmd(id, "ex_interp %d", interp);
}
HLTV event
Pierwszym, użytecznym zdarzeniem, jakie warto zanotować, to event HLTV.
register_event("HLTV", "newRound", "a", "1=0", "2=0");
Jest to zdarzenie, które wysyłane jest do HLTV, jednak można je wykorzystać,
by ustalić moment rozpoczęcia Freezetime, bądź, jak kto woli, początku rundy.
Wziąć należy jednak pod uwagę, że zdarzenie wykonywane jest, poza początkiem
rundy, także tuż po zakończeniu wykonywania się komend startowych serwera.
Uwaga! Podczas, gdy forwardy takie, jak client_authorized czy client_disconnect,
event HLTV ejst eventem globalnym, a więc wywoływany jest jednokrotnie i nie przyjmuje parametru gracza.
Oznacza to, że użycie newRound(id) jest nieprawidłowe, gdyż id nie jest identyfikatorem gracza.
Spawn graczy
Spawn, czyli pojawienie się gracza na mapie, jest zdarzeniem, które powinniśmy wykorzystać,
jeśli chcemy operować na graczu, zanim ten przystąpi do gry, ale po rozpoczęciu nowej rundy.
to tutaj dodajemy bonusowe bronie, lub ustawiamy dodatkowe życie dla danego gracza.
W celu kompleksowej rejestracji spawnu, skorzystamy z eventu Ham_Spawn funkcji RegisterHam z biblioteki HamSandwich.
RegisterHam(Ham_Spawn, "player", "spawned", 1);
Pierwszy parametr oznacza typ rejestrowanego zdarzenia, w tym przypadku spawn, drugi parametr to klasa bytu.
Byty
I tutaj warto wspomnieć o bytach. Otóż niemal każdy obiekt na mapie to byt, czyli istota wirtualna.
Ponadto, każdy byt ma swój unikalny numer identyfikacyjny, klasę i pewne właściwości, jak np. model.
Specjanymi bytami są gracze o numerach identyfikacyjnych od 1 do maksymalnej liczby graczy,
których maksymalną liczbą może być 32. Każdy byt posiada pewną klasę, klasą gracza jest "player".
Ponadto, każdy byt posiada pewne dane prywatne, jak np. model, życie, czy czas następnego procesu myślenia.
Proces myślenia to zdarzenie cykliczne wykonywane co pewien czas, który może być modyfikowany.
Przykładowo, granat wybuchowy posiada proces myślenia, który dokonuje jego eksplozji.
Modyfikacja czasu procesu myślenia tego bytu, czyli wpływanie na dane prywatne bytu,
pozwoli na szybszy, bądź wolniejszy wybuch granatu, lub całkowitą blokadę wybuchu.
Pobieranie danych prywatnych z bytu możliwe jest dzięki następującym funkcjom.
Na forum znajdziemy listę niektórych danych prywatnych bytów w temacie R3Xa, Offsety pdata.
Większość funkcji tak naprawdę operuje na bytach, jak np. funkcja cs_get_user_money,
która pobiera jedynie wartość prywatną od określonego gracza.
Spawn graczy, c.d.
Trzecim argumentem użytej funkcji RegisterHam jest nazwa funkcji, która zostanie wywołana po spawnie,
a ostatni argument ustawiony na 1, odpowiada za wykonanie funkcji po skończonym procesie ożywiania gracza.
W przeciwieństwie do zdarzenia HLTV, funkcja spawned przyjmie za argument id gracza ożywianego.
Jednakże, funkcja spawnu wykonywana jest także podczas wchodzenia gracza na serwer, będącego
jeszcze martwym, dlatego chcąc operować na właśnie ożywionym graczu, należy sprawdzić, czy żyje.
Funkcja Ham_Spawned dla każdego gracza wykonywana jest zawsze po evencie HLTV, ale przed startem rundy.
Start rundy
Po ożywieniu wszystkich graczy i zakończeniu czasu zamrożenia (freezetime), następuje event globalny Round_Start.
register_logevent("roundStart", 2, "1=Round_Start");
Jest to zdarzenie notowane, więc użyjemy funkcji register_logevent, filtrując drugi parametr do równego "Round_Start"
Koniec rundy
Alternatywnie do początku rundy, koniec rundy rejestrujemy dzięki informacji logowanej.
register_logevent("roundEnd", 2, "1=Round_End");
Koniec rundy może nastąpić na wskutek wielu innych czynników, co należy mieć na uwadze.
Ponadto, koniec rundy nie musi nastąpić nigdy, lub nastąpić rzadziej, niż początek rundy.
Reset rundy
register_logevent("GameCommencing", 2, "1=Game_Commencing");
Każdorazowo, w momencie dołączenia do gry pierwszego gracza do drużyny, której przeciwna posiada już wrogów,
dokonywany jest reset gry poprzez wywołanie eventu GameCommencing. Należy to wziąć pod uwagę przy liczeniu rund.
Liczenie rund
W celu policzenia rundy, potrzebować będziemy zmiennej globalnej, którą zainicjujemy tuż po imporcie bibliotek.
#include <amxmodx>Zmienna globalna zostanie początkowo zainicjowana wartością zero, co nam odpowiada, gdyż runda się jeszcze nie rozpoczęła.
new runda;
public plugin_init(){
register_plugin("Liczenie rund", "0.1", "benio101");
}
Co rundę będziemy inkrementować zmienną runda, a zrobimy to w funkcji newRound eventu HLTV.
#include <amxmodx>Jednakże, ponieważ funkcja HLTV wykonywana jest tuż po starcie serwera,
new runda;
public plugin_init(){
register_plugin("Liczenie rund", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
}
public newRound(){
++runda;
}
a po dołączeniu zazwyczaj drugiego gracza, wykonywany jest reset, przez co ponownie
wywoływany będzie event HLTV, rozpoczęlibyśmy grę właściwą ze zmienną runda równą 2 zamiast 0.
Dlatego też zarejestrujemy event Game_Commencing i w momencie jego wystąpienia, zresetujemy licznik rund.
I tym samym sposobem, uzyskamy zmienną runda, która przechowuje numer aktualnej rundy:
#include <amxmodx>
new runda;
public plugin_init(){
register_plugin("Liczenie rund", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
Studium pluginu: Bonus pieniężny
Teraz, w ramach praktyki, napiszemy prosty plugin, który w 2., 5. i 10. rundzie,
jednemu, losowemu graczowi przyzna 5000 dolarów na zachętę lepszej gry.
Ponadto, postaramy się, by jeden gracz nie uzyskał nagrody kilkukrotnie.
Zaczniemy od powyższej bazy, liczącej rundy, będzie to nam potrzebne.
Dodamy event nowej rundy i kod będzie wyglądał następująco:
#include <amxmodx>Zacznijmy od warunku. Najprostsza metoda:
new runda;
public plugin_init(){
register_plugin("Bonus pieniezny", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("roundStart", 2, "1=Round_Start");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
public roundStart(){
// dodanie w 2., 5. i 10. rundzie losowemu graczowi 5000$
}
public roundStart(){Jednak niepotrzebnie się powtarzamy, stąd pomysł na
if(runda==2){
// dodanie losowemu graczowi 5000$
}
if(runda==5){
// dodanie losowemu graczowi 5000$
}
if(runda==10){
// dodanie losowemu graczowi 5000$
}
}
public roundStart(){to rozwiązanie jest dobre, ale nie najlepsze. Dlaczego?
if(runda==2 || runda==5 || runda==10){
// dodanie losowemu graczowi 5000$
}
}
Dokonujemy tak naprawdę trzech porównań, choć zawsze porównujemy zmienną runda.
Switch
Do porównywania jednej zmiennych do kolejno wielu różnych, nadaje się idealnie funkcja switch.
Dzięki zastosowaniu funkcji switch, zaoszczędzimy zasoby sprzętowe i skrócimy czas wykonywania skryptu. Poprawne rozwiązanie:
public roundStart(){
switch(runda){
case 2,5,10:{
// dodanie losowemu graczowi 5000$
}
}
}
Teraz przejdziemy do losowania gracza. w tym celu będziemy musieli poszukać wśród żywych graczy na serwerze poprzez funkcję for.
W tym celu utworzymy sobie tablicę gracze i w niej będziemy przechowywać numery identyfikacyjne osób, kwalifikujących się do losowania.
Dodatkowo, utworzymy zmienną liczbaGraczy, która będzie przechowywać liczbę kandydatów do nagrody, czyli, de facto, żywych graczy.
każdy gracz, który jest żywy, zostanie dodany to tablicy gracze, a następnie liczbaGraczy ulegnie inkrementacji.
#include <amxmodx>Jednakże, maksymalna liczba graczy nie musi wynosić 32.
new runda;
public plugin_init(){
register_plugin("Bonus pieniezny", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("roundStart", 2, "1=Round_Start");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
public roundStart(){
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=32; ++id){
if(is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
}
}
}
Maksymalna liczba graczy (czy też liczba slotów), może być inna.
get_maxplayers
Aby pobrać liczbę slotów, użyjemy funkcji get_maxplayers.
public roundStart(){Jednakże, przy każdej iteracji pętli for, dokonujemy zapytania, poprzez MetaModa:P
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=get_maxplayers(); ++id){
if(is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
}
}
}
aż do silnika gry, co jest istotnym błędem i powinniśmy to zoptymalizować.
W tym celu, zainicjujemy zmienną globalną maxPlayers i uzupełnimy ją
w funkcji plugin_cfg i nie będziemy musieli więcej zmiennej pobierać.
Jest to rozwiązanie na tyle dobre, że funkcja plugin_cfg wykonywana jest tylko jednokrotnie
i dzięki pobraniu liczby slotów do zmiennej globalnej, oszczędzimy kolejnych, identycznych
zapytań, do silnika gry. Oczywiście, w naszej pętli, zastąpimy funkcję get_maxplayers zmienną maxPlayers.
#include <amxmodx>
new runda, maxPlayers;
public plugin_init(){
register_plugin("Bonus pieniezny", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("roundStart", 2, "1=Round_Start");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public plugin_cfg(){
maxPlayers=get_maxplayers();
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
public roundStart(){
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=maxPlayers; ++id){
if(is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
}
}
}
Losowanie
Teraz przejdziemy do losowania zwycięzcy.
W tym celu, użyjemy funkcji random, która dla jedynego argumentu x,
przyjmuje pseudo-losową liczbę naturalną z zakresu [0;x)
Funkcja jest świetnie przystosowana do naszej tablicy gracze,
przyjęwszy za argument zmienną liczbaGraczy, od razu wyłoni nam zwycięzcę.
public roundStart(){Podobną funkcją do random, jest funkcja random_num,
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=maxPlayers; ++id){
if(is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
new zwyciezca=gracze[random(liczbaGraczy)];
}
}
}
która losuje liczbę z zakresu od pierwszego argumentu, do drugiego włącznie.
Funkcja random jest zatem szczególnym przypadkiem funkcji random_num.
random(x) ⇔ random_num(0, x-1)
Pomimo jednoznaczności, zawsze używanie funkcji random_num z pierwszym argumentem równym zero zamiast
jego prostszej wersji random, jest irracjonalne, gdyż trwa dłużej i zużywa więcej zasobów sprzętowych.
Nagroda
Mając wyłonionego zwycięzcę, dodamy mu 5000$ i poinformujemy o tym jedynie zwycięzcę, by innym nie było smutno.
W tym celu, będziemy tak naprawdę modyfikować jedną z informacji prywatnych pdata zwycięskiego gracza, co można
byłoby zrobić funkcją set_pdata_int i get_pdata_int, jednak skorzystamy z prostszych natywów biblioteki cstrike.
#include <amxmodx>
#include <cstrike>
new runda, maxPlayers;
public plugin_init(){
register_plugin("Bonus pieniezny", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("roundStart", 2, "1=Round_Start");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public plugin_cfg(){
maxPlayers=get_maxplayers();
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
public roundStart(){
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=maxPlayers; ++id){
if(is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
new zwyciezca=gracze[random(liczbaGraczy)];
cs_set_user_money(zwyciezca, cs_get_user_money(zwyciezca)+5000);
client_print(zwyciezca, print_chat, "Na zachete lepszej gry, wygrales 5000 dolarow!");
}
}
}
#define
Zamiast pisać długą linijkę
cs_set_user_money(zwyciezca, cs_get_user_money(zwyciezca)+5000);zwłaszcza, jeśli wykorzystywalibyśmy ją wielokrotnie,
warto stworzyć sobie definicję preprocesora, udającą funkcję dodawania pieniędzy.
Zaimplementujemy zatem funkcję add_user_money(id, ammount). W tym celu,
stworzymy pod importowanymi bibliotekami, definicję preprocesora:
#define add_user_money(%1,%2) cs_set_user_money(%1,cs_get_user_money(%1)+%2)
I w miejsce starej, długiej linijki, wstawimy nowo utworzoną funkcję preprocesora add_user_money
#include <amxmodx>
#include <cstrike>
#define add_user_money(%1,%2) cs_set_user_money(%1,cs_get_user_money(%1)+%2)
new runda, maxPlayers;
public plugin_init(){
register_plugin("Bonus pieniezny", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("roundStart", 2, "1=Round_Start");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public plugin_cfg(){
maxPlayers=get_maxplayers();
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
public roundStart(){
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=maxPlayers; ++id){
if(is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
new zwyciezca=gracze[random(liczbaGraczy)];
add_user_money(zwyciezca, 5000);
client_print(zwyciezca, print_chat, "Na zachete lepszej gry, wygrales 5000 dolarow!");
}
}
}
Limitacja nagród
Teraz dodamy ostatnią rzecz, czyli ograniczymy możliwość wygrania kilkukrotnie przez tę samą osobę.
W tym celu utworzymy sobie tablicę globalną typu logicznego i będziemy przechowywać informację o tym,
który gracz wygrał już swoją nagrodę. Łącznie, nasz plugin ma teraz dwie zmienne i jedną tablicę logiczną:
new runda, maxPlayers, bool:nagrodzeni[33];
W momencie sprawdzania danego gracza w pętli, dodamy warunek na brak bycia nagrodzonym:
public roundStart(){Dlaczego jednak warunek w koniunkcji umieściłem po lewej stronie?
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=maxPlayers; ++id){
if(!nagrodzeni[id] && is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
new zwyciezca=gracze[random(liczbaGraczy)];
add_user_money(zwyciezca, 5000);
client_print(zwyciezca, print_chat, "Na zachete lepszej gry, wygrales 5000 dolarow!");
}
}
}
Działanie koniunkcji i alternatywy
Koniunkcja działa w taki sposób, że spełniona jest, gdy wszystkie warunki są spełnione.
Gdy choć jeden z nich nie jest spełniony, to cała koniunkcja nie jest spełniona i
nie ma potrzeby sprawdzać kolejnych warunków. Podobnie jest z alternatywą, jeśli
choć jeden z warunków zostanie spełniony, nie ma potrzeby sprawdzać kolejnych.
Optymalizacja warunków logicznych
Tak samo działa nasz AMXX, jeśli jeden z warunków koniunkcji nie zostanie spełniony,
to kolejne nie są sprawdzane. Istotne uwagi jest, że warunki sprawdzane są kolejno od lewej strony.
Wiedząc to, powinniśmy najbardziej prawdopodobne warunki koniunkcji ustalać możliwie najbardziej po prawej stronie,
a te mniej prawdopodobne po lewej, a dla alternatywy zaś odwrotnie, warunki najbardziej prawdopodobne winny być po lewej stronie.
Unikanie zapytań do HLDS
Dlaczego więc umieściłem nasz warunek !nagrodzeni[id] po lewej stronie, gdy wydaje się, że jest on bardziej prawdopodobny,
niż to, że gracz jest martwy? Ponieważ staram się możliwie unikać mocno obciążających maszynę i długich zapytań do
silnika HLDS poprzez Metamoda:P, a funkcja is_user_alive, niewątpliwie do takich należy, dlatego zawsze należy
wszelkie warunki nie wykonujące zapytań do silnika, umieszczać możliwie po lewej stronie koniunkcji czy alternatyw.
Kolejne warunki studium
W przypadku braku odpowiednich kandydatów, nie dodajemy losujemy zwycięzcy, nie dajemy pieniędzy ani nie informujemy gracza o zwycięstwie.
W tym celu, musimy te 3 instrukcje wykonać warunkowo, o ile zmienna liczbaGraczy jest niezerowa.
if(liczbaGraczy){Tworzenie zapytania if(x!=0) gdzie x to warunek jest niepotrzebne i wystarczy samo if(x)
new zwyciezca=gracze[random(liczbaGraczy)];
add_user_money(zwyciezca, 5000);
client_print(zwyciezca, print_chat, "Na zachete lepszej gry, wygrales 5000 dolarow!");
}
Ostatecznie, funkcja if i tak zwraca prawdę lub fałsz, jedynkę lub zero.
Zwycięzcę trzeba oznaczyć jako wygranego, więc wewnątrz warunku, dopisujemy
nagrodzeni[zwyciezca]=true;
Od teraz, do końca mapy, gracz nie będzie miał szansy wygrać 5000 dolarów.
Jednakże, zwycięski gracz mógł w międzyczasie wyjść z serwera, a w miejsce jego numeru identyfikacyjnego mógł wejść inny gracz.
Dlatego, w momencie wchodzenia gracza na serwer, ustawimy mu wartość zwycięstwa na false, by miał szansę brać udział w losowaniu.
Możliwość reconnecta zwycięzcy nie przeraża nas pomimo otrzymania kolejnej szansy na zwycięstwo, gdyż całą gotówkę w
momencie opuszczenia serwera straci, a jeśli serwer korzysta z pluginu, zapisującego stan gracza, jak np. pieniędzy,
można dodać prosty natyw do tego pluginu, który zapisywałby także i tę, zerojedynkową informację o graczu.
W tym celu, skorzystamy z forwardu client_putinserver, ustawiając zmienną nagrodzeni na fałsz
public client_putinserver(id){
nagrodzeni[id]=false;
}
Gotowy kod z naszego studium, plugin na bonus 5000$ dla losowego gracza z limitem 1 wygranej na mapę w 2., 5. i 10. rundzie:
#include <amxmodx>
#include <cstrike>
#define add_user_money(%1,%2) cs_set_user_money(%1,cs_get_user_money(%1)+%2)
new runda, maxPlayers, bool:nagrodzeni[33];
public plugin_init(){
register_plugin("Bonus pieniezny", "0.1", "benio101");
register_event("HLTV", "newRound", "a", "1=0", "2=0");
register_logevent("roundStart", 2, "1=Round_Start");
register_logevent("GameCommencing", 2, "1=Game_Commencing");
}
public plugin_cfg(){
maxPlayers=get_maxplayers();
}
public newRound(){
++runda;
}
public GameCommencing(){
runda=0;
}
public roundStart(){
switch(runda){
case 2,5,10:{
new gracze[32];
new liczbaGraczy;
for(new id=1; id<=maxPlayers; ++id){
if(!nagrodzeni[id] && is_user_alive(id)){
gracze[liczbaGraczy]=id;
++liczbaGraczy;
}
}
if(liczbaGraczy){
new zwyciezca=gracze[random(liczbaGraczy)];
add_user_money(zwyciezca, 5000);
client_print(zwyciezca, print_chat, "Na zachete lepszej gry, wygrales 5000 dolarow!");
nagrodzeni[zwyciezca]=true;
}
}
}
}
public client_putinserver(id){
nagrodzeni[id]=false;
}
Użytkownik benio101 edytował ten post 09.12.2012 02:59
+kot.