Taas sitä mennään. Näyttää siltä että yksi suuri ongelman ydin on kokoajan ollut (väärä) tapani käsitellä metodien pointtereita. Taas hieman pohjustusta ennen kysymystä:
Kun peliä "prosessoidaan", esin renreöintimoduuli hoitaa piirtämisen ja sitten antaa mahdollisuuden entiteeteille omien touhuilujensa suorittamiseen. Mutta synkronoinnista johtuen entiteetit eivät voi prosessoida itse itseään (kun sanon prosessoida niin tarkoitan että entiteetin Processor() -metodi ajetaan). Sen sijaan entiteetti voi lisätä oman Processor() -metodinsa erityiselle listalle, josta renderöintimoduuli yksi kerrallaan kutsuu sitten näitä metodeja. Koodia tähän väliin:
Type TSimpleEvent = procedure of object; {Yksinkertainen tyyppimääritys} ... var SceneProcessProcs : Array of TSimpleEvent; {Tämä on prosessorilista} ... {Tällä metodilla entiteetit voivat lisätä oman Processor() -metodinsa listaan} procedure AddSceneProgressCall(PCallProc: TSimpleEvent); var i : Integer; begin {Varmistetaan että metodi ei ole valmiiksi listassa} for i := 0 to High(SceneProcessProcs) do if @SceneProcessProcs[i] = @PCallProc then {VIITE 1.} Exit; {Jos listassa on vapaa paikka, eli joku solu on Nil sitten lisätään parametrina annettu metodi prosessorilistaan} for i := 0 to High(SceneProcessProcs) do if not Assigned(SceneProcessProcs[i]) then begin SceneProcessProcs[i] := PCallProc; Exit; end; {Koska suoritus päätyi tänne tarkoittaa se sitä että metodi pitää lisätä mutta sille ei ole vapaata paikkaa listassa --> laajennetaan listaa} SetLength(SceneProcessProcs, High(SceneProcessProcs) + 2); SceneProcessProcs[High(SceneProcessProcs)] := PCallProc; end; {Tätä kutsumalla entiteetti poistaa metodin listasta} procedure DeleteSceneProgressCall(PCallProc: TSimpleEvent); var i : Integer; begin {Käydään läpi prosessorilistaa...} for i := 0 to High(SceneProcessProcs) do if @SceneProcessProcs[i] = @PCallProc then {Verrataan osoitteita} begin {Nyt indeksillä i tärppäsi. Nollataan prosessorilistan alkio, ettei kyseistä metodia enää kutsuttaisi} SceneProcessProcs[i] := Nil; Exit; end; end;
Olen ihan varma että en käytä oikein tuota @ - merkkiä. Mainittakoon vielä että Processor() -metodi, joka siis kuuluu tyypille TMapEntity ja joka luonnollisesti perityy kaikille sen lapsille on virtuaalinen.
Esimerkiksi jos yritän lisätä 2 entiteettiä, joiden tyyppi on TSplatSprite, vain ensimmäinen pääsee prosessilistalle, toinen jää kiinni tuplatarkastukseen (katso koodista VIITE 1.)
Tuossa vielä koodi missä TMapEntity käyttää näitä kahta metodia:
procedure TMapEntity.SetNeutral(Value: Boolean); begin if Value <> FNeutral then begin FNeutral := Value; if FNeutral then DeleteSceneProgressCall(Self.Processor) else AddSceneProgressCall(Self.Processor); end; end;
Normaali funktio-osoitin on vain yksi osoitin. Sen sijaan metodiosoitin sisältää kaksi osoitinta, osoittimen itse metodiin ja osoittimen Self-olioon. (Ks. System-unitin TMethod.) Ilmeisesti @-merkki tuottaa näistä ensimmäisen, jolloin luonnollisesti kaikki saman luokan instanssit tuottavat saman tuloksen.
program PtrSize; var Ptr: Pointer; MethodPtr: procedure of object; begin WriteLn(SizeOf(Ptr), ' < ', SizeOf(MethodPtr)); { Esim. '4 < 8' } end.
Jos funktion nimi on aina Processor ja kaikki oliot ovat TMapEntityn alaluokkia, suosittelisin ennemmin taulukollista TMapEntity-osoittimia, koska virtuaalinen funktio löytyy kyllä kutsuvaiheessakin. Sitähän varten se on virtuaalinen.
Jos kuitenkin haluat pysyä valitsemallasi tiellä tai mainitut ehdot eivät täyty, joudut hankkimaan käsiisi osoittimien osoitteet ja vertailemaan niiden sisältä dataa. Tässä tilanteessa Pascalin toimintatavat @-merkin ja ^-merkin kanssa ovat minulle tuntemattomia, mutta jos muuta tapaa ei löydy, niin ainakin wrapperi luultavasti pelastaa (en kokeillut):
type TSimpleEventCmp = record ptr: TSimpleEvent; end; function SimpleEventEqual(S1, S2: TSimpleEvent): Boolean; var A, B: TSimpleEventCmp; begin A.ptr = S1; B.ptr = S2; Result := (A = B); { Vertaillaan recordeja, pitäisi toimia paremmin } end;
Metabolix kirjoitti:
Normaali funktio-osoitin on vain yksi osoitin. Sen sijaan metodiosoitin sisältää kaksi osoitinta, osoittimen itse metodiin ja osoittimen Self-olioon. (Ks. System-unitin TMethod.) Ilmeisesti @-merkki tuottaa näistä ensimmäisen, jolloin luonnollisesti kaikki saman luokan instanssit tuottavat saman tuloksen.
Jos funktion nimi on aina Processor ja kaikki oliot ovat TMapEntityn alaluokkia, suosittelisin ennemmin taulukollista TMapEntity-osoittimia, koska virtuaalinen funktio löytyy kyllä kutsuvaiheessakin. Sitähän varten se on virtuaalinen.
Jos kuitenkin haluat pysyä valitsemallasi tiellä tai mainitut ehdot eivät täyty, joudut hankkimaan käsiisi osoittimien osoitteet ja vertailemaan niiden sisältä dataa. Tässä tilanteessa Pascalin toimintatavat @-merkin ja ^-merkin kanssa ovat minulle tuntemattomia, mutta jos muuta tapaa ei löydy, niin ainakin wrapperi luultavasti pelastaa (en kokeillut):--
Tuo TMethod saattaisi olla pelastava tekijä. Näin sen aikaisemminkin mutta en ymmärtänyt sen merkitystä.
Valitettavasti en oikein voi/halua tehdä TMapEntity-osoittimia, koska esim. paikallista pelaajaa kuvaavalla kameralla on myös oma Processor, mutta se on liian poikkeava olio että sitä voisi yleensä liittää luokkastruktuuriin. No periaatteessa voisi, mutta kaikkien uusien luokkien sitouttaminen TMapEntityyn söisi vain modulaarisuutta.
Pitää lukea vielä tuosta ^-merkistä lisää ja katsoa joskos TMethod pystyisi auttamaan.
Ainahan voit periyttää molemmat jostain vielä yksinkertaisemmasta:
type TProcessorClass = class procedure Processor; virtual; end; TMapEntity = class (TProcessorClass) procedure Processor; override; end; TCamera = class (TProcessorClass) procedure Processor; override; end;
Tässä vielä ainakin Free Pascalilla toimiva versio vertailusta, menee kyllä purkkakoodin puolelle mutta tuottaa oikean tuloksen.
type TProcOfObj = procedure of object; TProcOfObjW = record P: TProcOfObj; end; function ProcOfObjCmp(A, B: TProcOfObj): Boolean; var AA, BB: TProcOfObjW; AP, BP: ^Byte; I: Integer; begin AA.P := A; BB.P := B; AP := @AA; BP := @BB; for I := 1 to SizeOf(TProcOfObjW) do begin if (AP^ <> BP^) then begin Result := False; Exit; end; Inc(AP); Inc(BP); end; Result := True; end;
Jos periyttäminen yhteisestä kantaluokasta ei sovi, suosittelisin sitä vaihtoehtoa, että teet koodista bugitonta ja selviät aivan ilman vertailuja.
function AddCallToList(C: procedure of object): Integer; begin { Lisää kutsu taulukkoon, ei tarvitse tarkistaa olemassaoloa } Result := indeksi; end; procedure RemoveCallFromList(I: Integer); begin { Poista kutsu taulukon kohdasta I, se on varmasti oikea kohta } end; constructor TMapEntity.Create; begin { Objekti säilyttää oman indeksinsä; -1 = ei indeksiä } Self.CallNumber := -1; end; procedure TMapEntity.AddCall; begin { Lisätään vain, jos ei ole vielä lisätty } if Self.CallNumber < 0 then Self.CallNumber := AddCallToList(Self.Processor); end; procedure TMapEntity.RemoveCall; begin { Poistetaan, jos ei ole vielä poistettu } if not (Self.CallNumber < 0) then RemoveCallFromList(Self.CallNumber); Self.CallNumber := -1; end;
Metabolix kirjoitti:
--Jos periyttäminen yhteisestä kantaluokasta ei sovi, suosittelisin sitä vaihtoehtoa, että teet koodista bugitonta ja selviät aivan ilman vertailuja.--
Jep, tuossa olisi taas ratkaisun makua. En toteuttanut tuolla systeemillä alun perin koska lista toimi silloin vielä dynaamisesti eli tyhjiä soluja ei syntynyt, eli silloin indeksitkin muuttuivat. Mutta en tosiaan jaksa painia enempää tämän parissa, koko ongelma on niin vienyt koodaushalut pois, minulla kun on vielä sellainen perfektionistin asenne että kaikki pitää tehdä kirjan mukaan.
Tyhjiä solujahan ei synny listoihin ollenkaan jos siirtää viimeisen indeksin poistetun paikalle ja pienentää listan kokoa yhdellä. Tuossa vaiheessa kyseiseen indeksiin siirrettyyn objektiin viittaavat muut "asiat" pitää huolehtia päivittää. Sopisiko se tapaukseen?
User137 kirjoitti:
Tyhjiä solujahan ei synny listoihin ollenkaan jos siirtää viimeisen indeksin poistetun paikalle ja pienentää listan kokoa yhdellä. Tuossa vaiheessa kyseiseen indeksiin siirrettyyn objektiin viittaavat muut "asiat" pitää huolehtia päivittää. Sopisiko se tapaukseen?
Alun perin systeemi toimi niin että solua poistetaessa kaikki seuraavat solut "nakattiin" askeleen eteenpäin. Mutta ei tyhjät solut sinällään ole mikään ongelma -suurissakaan määrin- ja Metabolixin ehdottama malli sallisi yksinkertaisemman rajapinnan renderöintimoduulin ja entiteettien välille, kun renderöintimoduulin ei tarvisi huolehtia viitteistä.
Kaikki näyttäisi toimivan nyt niinkuin pitää. Keksin muuten että listaahan voi tietysti pitää pienenä kutistamalla sitä loppupäästä aina kun sinne syntyy tyhjiä soluja. Kaikki pitkään elävät entiteetit ovat kuitenkin listan alkupäässä.
Aihe on jo aika vanha, joten et voi enää vastata siihen.