Zkouším navrhnout Entity Component System v využitím šablon v C++20.
Nejprve se musím přiznat, že jsem vůbec nevěděl, že něco takového existuje, takže tenhle článek nebude mít edukativní charakter, ale spíš půjde o moje pokusy-omyly. Můj zájem o vývoj v oblasti herního průmyslu skončil někdy v roce 2007, což tak nějak koresponduje s tím, že jsem opustil Bohemia Interactive Studio a nastoupil na práci vývojáře v Seznamu. A celý můj další profesní vývoj se točil kolem serverů na linuxu. Má jediná zkušenost z plnohodnotného vývoje her je právě skrze hru Brány Skeldalu. Na dalších hrách, tedy na Operation Flashpoint a Arma Assault jsem se přímo nepodílel, můj příspěvek v týmu byl ve formě nástrojů na práci s modely – příprava 3D grafiky. Přechodem do Seznamu toto padlo
Na ECS mne upozornila umělá inteligence, když jsem při portaci hry Brány Skeldalu přemýšlel o rozšíření. Je třeba říct, že v době vývoje hry jsem znal jen následující jazykové nástroje: struktury a pole struktur. Samozřejmě jsem věděl něco o databázích, ale ty jsem považoval za velké pomalé obry, které se v efektivitě začnou vyplácet od milionu položek výše. S přechodem na C++ jsem byl plně pohlcen objektovým programováním, což z dnešního pohledu asi nebylo úplně nejšťastnější – nicméně nechci OOP úplně zatracovat. Protože předtím to byly struktury a pole struktur, potom to byly objekty, což je pořád velký krok pro programátora mého stylu. Nicméně že by existovaly jiné organizace dat mne asi hned nenapadlo.
… tedy já vím, že existují relační databáze, E-R schémata, organizace v tabulkách, 3. normální forma, atd. ale nic z toho se mi nehodilo pro hry.
Konkrétní dotaz: Jak hra Dungeons & Dragons Online managuje herní pravidla (postavená nad DnD), které obsahuje tisíce různých abilit, atributů zbraní, stejně velké množství kouzel, ale co je hlavní, vývojáři tento systém byly schopni časem rozšiřovat a to v rámci krátkého vývojového cyklu. Protože jsem jeden čas byl vášnivý hráč tohoto MMO, tak mne to samozřejmě zajímalo a fascinovalo.
Entity Component Systém je odpověď
(teď nevím, jestli DDO používá přesně ECS, nebo nějakou variaci, takže mne netahejte za slovo)
Dnes už vím, že to není jediná hra, že prakticky všechny hry staví na ECS. Protože nějaká forma ECS je dnes nedílnou součástí herních engine, jako Unity a Unreal. Takže by bylo asi nošení dříví do lesa ho zde podrobně představovat (i tak to udělám). Co mne tedy spíš zajímá, jak to že jsem ho tehdy nepoužil. Protože v roce 1996 prakticky neexistoval
První použití ve hře je právě až v roce 1998 ve hře Thief: The Dark Project. Zajímavý, že ECS nebyl použit ani Operation Flashpoint až do verze Dragon Rising v roce 2007 od Codemasters. Takže proč jsem ho nepoužil, jednoduše jsem ho neznal…
Pokud jde o obecné pojetí, jde o organizaci dat. Stejně jako v obecném E-R schématu, i tady jde o nějaké schéma. Nejedná se tedy o knihovnu, ani samostatně běžící server. Je to jen další alternativa třeba k OOP (k objektům). Toto schéma se skládá ze tří částí
ECS se výrazně liší od OOP. Zatímco v OOP máme objekt, jehož vlastnosti definuje třída, která poskytuje metody definující chování a tato třída může být děděna a to v případě v C++ ve vztahu N:M. Klasická představa OOP hierarchie „objekt<-živý objekt<-zvíře<-pes<-jezevčík<-můj pes“ se tady neuplatňuje. Naopak ECS víc připomíná duck typing. Entita „můj pes“ bude mít komponenty „štekot“, „pohyb“, „vrtění ocasem“, „tvar těla“,… atd… mezi nimi není žádná hierarchie.
Typické operace, které nad ECS provádíme je zpravidla dotazy: „dej mi všechny entity, které štěkají“. Případně kombinace dotazů: „dej mi všechny entity, které štěkají, a zároveň umí vrtět ocasem“. V těchto dotazech často ani nepotřebujeme vědět konkrétní identitu entity, stačí mi, že entita má společné vlastnost. Praktičtější příklad může být: „dej mi všechny entity, které mají mesh a polohu ve scéně“. Tenhle dotaz položí engine před tím, než se rozhodne vykreslit scénu. Jaké konkrétní entity to jsou, přitom není rozhodující, důležité jsou hodnoty v komponentách. Všechny úlohy, které musí systém řešit, jako je třeba právě rendering, ale i herní logika nad skupinou entit se smrskne na vhodně položený dotaz
Další odlišností od OOP je, že přiřazení komponent je dynamické. Entity tak mohou získávat a přicházet o komponenty a tím měnit význam. Udělám z psa kočku tím, že mu odeberu nějaké entity a jiné mu přidám.
┌──────┐ ┌──────┐ ┌──────┐
│ Ent- │ │ Ent- │ │ Ent- │
│ ti- │ │ ti- │ │ ti │
┌────────────┐ │ ty 1 │ │ ty 2 │ │ ty 3 │
│ │ │ │ │ │ │ │
│ Registry │ │ │ │ │ │ │
├──────┬─────┤ ┌────────────────────────────┐
│ id_C1│ ────┼───►│ ComponentPool<C1> │
├──────┴─────┤ └────────────────────────────┘
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
├──────┬─────┤ ┌────────────────────────────┐
│ id_C2│ ────┼───►│ ComponentPool<C2> │
├──────┴─────┤ └────────────────────────────┘
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
├──────┬─────┤ ┌────────────────────────────┐
│ id_C3│ ────┼───►│ ComponentPool<C3> │
├──────┴─────┤ └────────────────────────────┘
│ │ │ │ │ │ │ │
└────────────┘ │ │ │ │ │ │
Pro implementaci ECS v C++ lze použít knihovnu EnTT.
Ale sami víte, že takhle nefunguju. Musím totiž potrénovat mozek. Mozek je totiž sval a i ve stáří ho člověk musí používat. Mně příští rok bude půl století a nerad bych zakrněl. Já se prostě nespokojím jen s tím, že něco má řešení. Já sámsi musím vyzkoušet to vyřešit!
Kromě toho, EnTT neřeší úplně všechny požadavky, které na ECS mám, například snadné propojení s C nebo se skriptovacími enginy (Dá se to, ale není to tak čitelné a intuitivní). Kromě toho EnTT je maličko bloatware, obsahuje věci, které v ECS vyloženě nepotřebuji, nebo které jsem si schopen realizovat jinak, jinou knihovnou. No a úplně na závěr, EnTT staví na C++17. Hodně staví na RTTI (runtime type information), které navíc různě ohýbá, protože se v praxi ukázalo, že jej často uživatelé ve svých projektech vypínají. S příchodem C++20 se přitom hodně věcí v jazyce změnilo a dělá se to dnes jinak a lze se obejít bez RTTI úplně(a s příchodem C++26 se můžeme těšit na další skopičiny, jako například reflexi).
Takže jsem si napsal vlastní knihovnu ECSTL. A i tady jde o header only, na šablony bohatou a constexpr kompatibilní záležitost určenou pro C++20 a výše. Zbytek článku bude hlavně o tom, jaké problémy jsem řešil a jak jsem se s tím vypořádal
Tady je jeden lehce pitomý příklad „hello world“
#include "../ecstl/ecstl.hpp"
#include <iostream>
using namespace ecstl;
struct Greeting {
std::string text;
};
int main() {
Registry rg;
Entity greeter = rg.create_entity();
rg.set<Greeting>(greeter, {"Hello world"});
for (const auto &[e, c1] : rg.view<Greeting>()) {
std::cout << e << ": " << c1.text << std::endl;
}
return 0;
}
V příkladu zavádím Registry což je objekt, který drží celou „databázi“ všech entit a komponent. Na dalším řádku pak vytváříme entitu. Je zde reprezentovaná objektem Entity. Interně jde o automatické číslo, tohle je důležité jen při vypisování, ale jinak se prakticky s interní reprezentací nepracuje. Metodou rg.set nastavujeme komponentu Greeting právě vytvořené entitě i s hodnotou. A poslední operací je vypsání všech entit, které mají danou komponentu včetně hodnot
#1: Hello world
V tomto bodě to mám navrženo stejně jako v EntT. Předpokládáme, že komponenta je struktura. Obecně může být použit jakýkoliv běžný typ jako komponenta. Vždycky, když pracuji s komponentou, používám parametr šablony pro její specifikaci. Např.
Jak jsem psal výše, řešil jsem rozšiřitelnost hry Brány Skeldalu. Tato hra je napsaná v C, a proto, pokud bych chtěl přidělat nějaké rozšíření, musel bych si napsat nějaké C API. Ovšem v C přicházím o typovou informaci. Stejný problém nastane i u skriptovacích jazyků. Například pokud bych například rozšířil ECS na Python scripting, pak budu všechny jeho implementované komponenty typy PyObject (nebo wrapper kolem PyObject), což mi v compile time nepomůže.
Knihovna EnTT toto přímo moc neřeší. Přímo pro Python se to dá dělat tak, že konečný počet komponent definuji tak, že dědím Python wrapper, každá komponenta má jinou třídu, ale naprosto stejný základ. To znamená, že musím dopředu znát komponenty které může python script používat. Vlastní komponenty (a systém) si script přidat nemůže.
Já jsem na to šel jinak. Kromě typu lze totiž definovat i variant ID, kdy každá komponenta má variantu. Toto je volitelná funkce a pokud ji nepoužiju, všechny komponenty mají typ varianty s ID = 0. Příklad nastavení varianty pro greetera
constexpr auto anglicky = ComponentTypeID("anglicky");
constexpr auto cesky = ComponentTypeID("cesky");
Registry rg;
Entity greeter = rg.create_entity();
rg.set<Greeting>(greeter, anglicky, {"Hello world"});
rg.set<Greeting>(greeter, cesky, {"Ahoj svete"});
for (const auto &[e, c1] : rg.view<Greeting>({cesky})) {
std::cout << e << ": " << c1.text << std::endl;
}
for (const auto &[e, c1] : rg.view<Greeting>({anglicky})) {
std::cout << e << ": " << c1.text << std::endl;
}
Tímto způsobem mohu registrovat dvě komponenty typu Greeting na entitě, každá s jiným ID varianty. Všechny metody, které adresují komponenty tak mají ještě navíc parametr pro variantu, který je ve výchozím stavu 0 a takto lze nastavit jinou variantu.
Při návrhu rozhraní pro dotazy jsem se snažil maximálně se přizpůsobot existující knihovně ranges v C++ . Tato knihovna byla schválena do C++20 a povyšuje iterace a filtrace výsledků na nový level. Určitě doporučuji nastudovat. Objekt Registry tak v zásadě poskytuje přístup ke zdrojovým pohledům, které lze následně použít ve výrazech nad rozsahy (ranges)
variant. Výsledek se předává jako pairObě metody jsou jak v const tak v non-const variantě. V non-const variantě tak získáme referenci na obsah komponenty a ten lze měnit. Metody lze tedy použít nejen na čtení, ale i na modifikace
Typické použití je následující
for (auto &[e,c1,c2,c3] : rg.view<C1,C2,C3>({var_c1, var_c2,var_c3})) {
//e je entity, delej cokoliv s c1,c2,c3
}
Varianty var_c1, var_c2 a var_c3 mohou být vynechány pro výchozí variantu (případně lze uvést jen některé)
for (auto &[e,c1,c2,c3] : rg.view<C1,C2,C3>()) {
//e je entity, delej cokoliv s c1,c2,c3
}
Pokud potřebujeme výsledek dál filtrovat, lze použít ranges::filter
rg.view<C1,C2>() | std::ranges::views::filter(
[&](const auto &item){
return std::get<1>(item).x > 42;
});
Efektivita ECS stojí a padá na vhodně zvoleném úložišti. To musí být optimalizované zejména pro dotazy na komponenty napříč entitami. Cílem je mít data co nejvíc u sebe a maximalizovat využít CPU cache, proto je vhodné jednotlivé komponenty entit držet ve vektoru, který alokuje lineární prostor. K tomu se nehodí použít ani std::map ani std::unordered_map. Navíc protože se předpokládá, že komponenty mohou obsahovat vektorová data (3D engine), je seznam entit oddělen od seznamu komponent pro lepší aligmnent. Podobně funguje i flat_map z C++23, mimochodem tato šablona je dobrým kandidátem na úložiště, protože výše uvedené splňuje. Protože však se pohybuju v C++20 a mám s úložištěm ještě jiné záměry, implementoval jsem si vlastní variantu – IndexedFlatMap
Tato implementace používá dvou vektorů a jedné hashovací tabulky. Hashovací tabulka funguje jako index. Ve vektorech jsou uložené klíče (entity) a hodnoty (komponenty). Při iteraci se prochází současně oba vektory lineárně tak jak jsou data uložena. To řeší lokalitu dat. Hashovací tabulka slouží k rychlému vyhledání komponenty podle Entity. Obsahuje entity id a index komponenty ve vektoru.
std::unordered_map<K, std::size_t, Hasher, Equal> _index;
std::vector<K> _keys;
std::vector<V> _values;
┌───┬───┬───┬───┬───────────────────┐
Index │E1 │E2 │E3 │E4 │ │
(unordered_map) │───┼───┼───┼───┼───────────────────│
└─┬─┴─┬─┴─┬─┴─┬─┴───────────────────┘
│ │ │ │
└──┐│ │ └─────────┐
│└───┼─────┐ │
│ └─┐ │ │
▼ ▼ ▼ ▼
┌────────────────────────────────────┐
Klíče │E5 E1 E7 E3 E2 E6 E4 │
├────────────────────────────────────┤
Hodnoty │C5 C1 C7 C2 C3 C6 C4 │
└────────────────────────────────────┘
Protože jak entity tak komponenty jsou uložené v lineárním seznamu, je iterace velice jednoduchá a rychlá
Pohledy (view) obsahuje komponenty, které mají společné entity. Je to podobně jako JOIN operace v SQL jazyce. Pohledy lze procházet iterátory (zpravidla fungují jako input_iterator nebo forward_iterator)
V mém případě se používá iterate-and-lookup Před zahájením iterace se vybere to uložiště pro komponenty, které má nejmenší mohutnost. Tedy pokud napíšu rg.view<C1,C2,C3>(), zjistí se, ve které komponentě z C1, C2 a C3 je nejméně entit, a ta se začne iterovat. Každá entita se pak hledá v ostatních uložištích přes hashovací tabulku. Pokud není nalezena, přeskočí se a pokračuje se další entitou
Pojmenování archetyp jsem poprvé viděl v Unity. Myšlenka za archetypy je taková, že komponenty, jejichž pohled se často vytvářejí ve stejné konfiguraci by bylo vhodné držet pospolu už tako sloučené, aby se to nemuselo slučovat při každém volání znova. Tímto sloučením vznikne archetype. Unity používá archetype chunks, a jednotlivé komponenty často různě přesouvá mezi chunky podle toho, jak se zrovna používají.
V rámci zachování nějaké jednoduchosti jsem se vůbec nepokoušel něco takového replikovat, ale určitou jemnou optimalizaci jsem přeci jen zavedl. Týká se seskupování entit pro dané konfigurace pohledu tak, aby šly ve stejném pořadí.
Optimistická lineární iterace pohledem předpokládá, že úložiště (pooly) mohou být optimalizované tak, že entity sdílející stejnou konfiguraci komponent budou ve všech těchto úložištích ve stejném pořadí. Pokud tomu tak je, hashmapa se použije jen pro první hledání a následně se prochází úložiště lineárně. Pokud je toto narušeno, pro vyhledání dalšího výsledku se použije lookup přes hashmapu. Je tedy výhodné pokud pořadí je optimalizované
Pořadí vkládání komponent - může hrát roli. Je výhodné pokud vznikají entity, které mají nějakou konfiguraci komponent, aby vznikly současně a aby přiřazení komponent proběhlo také ve stejném pořadí. Například budu vytvářet 20 entity a každá bude mít 5 komponent, pak je ideální to dělat v pořadí: „vytvořit entitu a přiřadit komponenty, vytvořit entitu a přiřadit komponenty“. Pak mohou být současně iterovány v rámci optimistické iterace.
Operace group - je funkce rg.group<C1,C2,C3…>(), která provede interní reorganizaci komponent napříč entitami tak, aby výše uvedena optimistickou iteraci bylo možné použít. Toto není triviální operace, takže se ji vyplatí provést až na usazeném systému, a to pro všechny potřebné konfigurace komponent.
┌─────────────────────┐
│ E10 E11 E8 E3 E2 │
│ X10 X11 X8 X3 X2 │
└┬──────────────────┬─┘
│ │
└───────┐ └───────┐
│ │
│ │
┌────────┼──────────────────┼─────────┐
│ E7 E5 E10 E11 E8 E3 E2 E1 E9 │
│ Y7 Y5 Y10 Y11 Y8 Y3 Y2 Y1 Y9 │
└────────────────┼──────────┼─────────┘
│ │
┌───────────┘ │
│ ┌──────────┘
│ │
┌────┼───────────┼────────┐
│ E22 E8 E3 E2 E17 E55 │
│ Z22 Z8 Z3 Z2 Z17 Z55 │
└─────────────────────────┘
Vím, že to není archetype, ale podle mne to pomůže. Neměřil jsem to.
Už od počátku bylo v plánu aby celé Registry pracoval plně v constexpr. Na první pohled to moc nedává smysl, je to dynamický systém. Ale constexpr implementace umožňuje použít Registry pro výpočet jiných staticky alokovaných dat. Samotný Registry ale nejde v constexpr připravit aby se dal použít v runtime, tak daleko se svět constexpr ještě nedostal.
Druhým důvodem je agresivnější optimalizace. Constexpr výrazy dávají překladači do rukou nástroje na lepší optimalizace a výpočty, kde některé výsledky lze získat již během překladu.
A posledním důvodem jsou testy. Cílem bylo, aby testy se spouštěly během kompilace. Více o tomto způsobu testování najdete v mém článku „Nechte testovat překladač“. Shrnu jen výhody:
Ukázalo se, že C++20 uběhlo velký kus cesty k plné podpoře constexpr kompilací, ale ještě zdaleka není u cíle. Některé věci v C++20 nejde vyřešit nad STL a vyžaduje custom implementaci. To je asi největší zádrhel
Postupné vychytávání much probíhalo přenášením částí zdrojového kódu do godbolt a překlad ve všech dnešních překladačích současně. Většinou jsem musel sáhnout po vlastní implementaci
Vznikl adresář pollyfill/ ve kterém jsem implementoval unique_ptr<T>, tato implementace je má vlastní jen pro C++20, protože od C++23 už je constexpr definován.
Stejně tak jsem definoval hash<T> pro některé typy, které v knihovně používám, typicky hash<std::string_view>, používá FNV-1a hash
Náhradu unordered_map jsem vyřešil implementací vlastní hashovací mapy s otevřenou adresací plně constexpr kompatibilní. Vzhledem k tomu, že se používá k ukládání indexu, tak tato implementace stačí.
Jako náhradu RTTI jsem použil nový mechanismus generování hashe z názvu třídy. Použije se source_location což je nový objekt v C++20. Funkce current() vrací const char * s názvem funkce, ve které se volání nachází a to včetně názvy třídy, jejíž metoda je součástí… a pokud jde o šablonu, pak místo parametru pak najdeme třídu, jejíž jméno hledáme.
Velice jednoduše se dá generovat hash z názvu třídy takto:
template<typename T>
struct ClassIdent {
static constexpr std::string_view get_ident_string() {
return std::source_location::current().function_name();
}
static constexpr std::size_t get_hash() {
hash<std::string_view> hasher;
return hasher(get_ident_string());
}
};
template<typename T>
constexpr auto class_hash = ClassIdent<T>::get_hash();
template<typename T>
constexpr auto class_ident_string = ClassIdent<T>::get_ident_string();
Pohrát si s tím můžete zde: godbolt
Tak tohle je moje uchopení ECS v C++. Celou knihovnu najdete samozřejmě na githubu. Klidně napište, co by se dalo přidat, co jsem pochopil špatně a jaké další funkce by měla knihovna umět.
Děkuji za zajímavý článek, který jde příjemně pod povrch - a za rozšíření obzorů (jsem hobbík).
Odpusťte mi povrchní komentář:
Jste-li jako programátor postaven před dostatečně složitý problém = systém k programátorskému pojednání, pravděpodobně pocítíte touhu, napsat si abstrakční vrstvu, která Vás oprostí od nutnosti modelovat data tupě "at compile time". Jinak řečeno, napsat si vlastní prostředí, které Vám umožní dále modelovat data a tvořit, bez úprav kompilovaných zdrojáků. Napsat si svůj vlastní domain-specific KV store / adresářový engine / grafovou databázi, odpoutanou od elementárního zemitého folklóru C/C++.
Pokud správně chápu, ECS je někde na půl cestě k plnotučné abstrakční vrstvě. Hovoříte k němu ve zdrojáku C++, ale jako páteř ECS používáte jakýsi "sjednocený prostý metamodel", ačkoli zároveň prokazujete respekt statickému typování (šablonám povinně udáváte typ každého "drobku", který do ECS hodláte vkládat).
Zajímavá záležitost. Chovám respekt k Vašemu "method artist" přístupu, k Vašemu smyslu pro ohavný detail (celá knihovna v headerech/šablonách, honba za constexpr, a jak jdete naproti efektivitě cache). Tohle se dneska moc nevidí a nenosí... Mít víc času, ponořil bych se do návazné četby k těmto postranním zápletkám. Opravdu se Vám to povedlo :-)
Děkuji za uznání.
Nejsem si jist, jestli jsem úplně pochopil težiště dotazu, ale zkusím na něco reagovat.
ECS jsem si nevymyslel, tohle je celkem dobře probraná věc. Pro studium stačí začít na wikipedii: https://en.wikipedia.org/wiki/Entity_component_system
Ve zkratce ECS je v zásadě jednoúčelová databáze. ECS by se dal v relační databázi realizovat zřejmě jako tabulky propojené přes jeden cizí klíč typu sequence (Postgresql terminologie). Prakticky by to asi bylo pomalé, protože se to píše takhle speciálně jak jednoúčelová databáze. A pak už je to o tom, jaká se zvolí strategie v implementaci, co je cílem, kde chceme vytáhnout výkon.
Tato jednoúčelovost se hodí zejména tam, kde mám definované nějaké ability systému, a mám nějaké entity, kterým tyto ability přiřazuji. Speciální významy pak mohou mít kombinace abilit (komba). Jednotlivé ability nic nědělají, ale pokud na jedné entitě zkombinuju tyto dvě ability, stane se něco úžasného. Je to výborný základ pro hry, ale třeba i pro nějakou evidenci uživatelských rolí, vlastností objektů, atd. Co se třeba nedá dělat tak to jsou vztahy 1:N kdy třeba nějaká entita má jednu komponentu a k ní N variant jiné komponenty (i když by to šlo řešit právě přes čísla variant, ale to je trochu kanón na vrabce).
ECS je navržena jako in-memory databáze. Je tedy možné ji perzistovat na disku, ale automaticky to není zaručeno, vyžaduje to další programování. Šlo by použít reflexe nad komponentami, ale k tomu se dostaneme až v C++26. Nebo použít jiný jazyk, třeba Javu :) Nicméně perzistence ECS databáze u nějaké hry je v zásadě uložená pozice. A tím se asi věc zjednoduší (třeba důvod, proč je BG3 schopen uložit hru uprostřed souboje , dialogu nebo in-game cutscény - celý stav mají v nějaké takové databázi.
Co se týče implementace v C++20, tak to je zase moje doména. Osobně považuji dvacítku za revoluci (byť už by to chtělo přejít na 23 - bohužel překladače nejsou pořád úplně ready, ale je to lepší a lepší)
Použití šablon umožňuje dále systém customizovat. Například lze zvolit jiný způsob úložiště pro konkrétní typu komponenty - stačí pouze specializovat danou šablonu a začne to magicky fungovat. Lze nahradit i úložiště celého Registry
A v neposlední řadě je to o překladačích. Dávno je pryč doba, kdy překladače překládaly kód přesně 1:1. Jazyk se vyvíjí postupně do podoby, kdy budeme definovat požadavky na systém a okrajové podmínky chování ale výsledný kód může pro daný problém být úplně jiný, specifický, bude dělat to co potřebujeme, ale nebude přesně odpovídat tomu, co původní programátor zamýšlel. Překladač si prostě vyhodnotí, že něco se dá vynechat, někde se něco přepíše jinak, pro daný problém lépe vyhovujícím stylem. S příchodem umělé inteligence do překladačů teprve uvidíme věci. Každopádně platí, že čím víc dám překladači informací, tím víc prostoru má na optimalizace. Proto header only. Proto šablony. A pokud mu některé metody umožním i v constexpr, pak je to ještě lepší, dávám překladači svolení, že části, co se dají spočítat už během překladu se prostě během překladu spočítají a tím se zase ušetří výkon.
Takže snad to je uspokojivá odpověď
Děkuji za osobní reakci :-) Popravdě já jsem ke svému "názoru" naschvál nedal otazník - ale Vaše odpověď rozhodně potěšila.
V tuto chvíli píšu lehce mimo téma, že mi Youtube cca předevčírem nadhodil následující přednes:
https://www.youtube.com/watch?v=wo84LFzx5nI
Kupodivu clickbaitový titulek moc nesedí k osahu :-) Klikl jsem na to spíš znuděně a s odporem k k tomu klišé v titulku, a pak jsem se nestačil divit, jaký kus krásného dějepisu ta přednáška obsahuje. Jinak nic moc teoreticky hlubokého... a je tam i docela dlouhý kus věnovaný právě ECS a jeho roli ve hrách od Looking Glass, kde Casey Muratori nějaký čas pracoval. Až mi vrtalo hlavou, jestli mi YouTube to téma nahodil záměrně, se znalostí mé čerstvé brouzdací historie tady na rootu :-D Na soustředěné sledování je to video trochu rozvláčné, ale jako pozadí k manuální domácí práci posloužilo velmi dobře.
Jako nehobík bych řekl, že touze napsat si nějakou takovou abstrakční vrstvu je záhodno odolat. Obzvlášť pokud řešíte nějaký dostatečně složitý problém.
O nějakých flexibilních strukturách za běhu se přemýšlí podstatně obtížněji, než o rigidnějších "compile-time" strukturách. Kompilátor křížený s dominou je skvělá věc, protože co nedovolí, to opravdu nemůže nastat. Čím víc můžu tvořit za běhu, tím míň o těch datech můžu předpokládat. Najednou už vůbec nemůžete modelovat a tvořit, protože nemáte tušení, co se vlastně děje.
Pro jednoduché a krátkodobé projekty je dynamický přístup super. Ale jak jde veliký projekt, který se nedá udržet v hlavě a navíc dlouhodobá údržba znamená že spoustu věcí zapomenete, tak se z takové abstraktní vrstvy spíš zblázníte. Proto máme třeba typescript, nebo typové anotace v pythonu.
To je pravda, na webech jsem taky přešel na typescript. Už jenom to, že vscode intellisence umí správně napovídat, ví co kam patří, podtrhne co kam nepatří, a dokonce copilot odhadne, co chci dál napsat (a napíše to za mě :)
Na první bych reagoval takto: https://en.cppreference.com/w/cpp/compiler_support/23.html
- je tam prostě příliš mnoho červených míst i u clangu a gcc, a navíc na některých strojích mám ještě pořád gcc-13. Update asi přijde s novou LTS distribucí (nejspíš ubuntu). Takže je to praktický důvod. U jednoho zákazníka, pro kterého programuji dlouhodobě je aktuálně schváleno gcc-10.3.
A to je C++23. Nevím v jakém stavu schvalování je C++26. Ale support je ještě tristní: https://en.cppreference.com/w/cpp/compiler_support/26.html
- je tam víc chyb, postupně to opravím :)
Co se týče hive - musel jsem se podívat, jak přesně funguje - nejsem si jist, ale možná pokud bych zavedl archetypy, kde bych kombinace komponent přesouval do separátních poolů, pak by to asi šlo nějak použít.
Já tam aktuálně používám pro mazání strategii swap-and-truncate, tedy neděje se přesun všech prvků, jen jednoho. Ano, naruší to pořadí, ale zase to není velký issue. Výhodou je, že počet prvků zjistím tak, že odečtu end od begin, což se hodí, když se hledá pool s nejmenší mohutností. Pak je ještě výhoda v iteraci, že iterátor je vlastně random_access nebo aspoň bidirectional. To umožňuje range pro all_of<C> (enumerace entit dané komponenty) procházet náhodným přístupem. Oproti tomu samotný pohled (view<C>) už je input iterátor právě kvůli přeskakování. Jestli jsem pochopil hive taky přeskakuje, takže by se tam iterace maličko zesložitila.
Hive ovšem neřeší lookup, takže pořád potřebuji index. Index obsahuje pozice všech prvků - k tomu se právě hodí random access iterátor.
U hive je zaručeno zachování raw ukazatelů na jednotlivé platné objekty. Udržuje se myslím seznam vektorů a pamatuje nějak prázdná místa při mazání. Nové přidávané vektory se s roustoucím počtem objektů zvětšují až na nějaké maximum. Sekvenční přístup je přes seznam vektorů, takže je to v zásadě cache friendly. I tak úplně nevím jak bych to využil, a tak různě sonduju. Ostatně dostalo se to do standardu, tak to musí mít nějaký důvod.
tak určitě to smysl má. Hodně mi to připomíná alokátor malých objektů od Andrei Alexandrescu, není to tedy poprvé co bych něco takového viděl. Tedy jestli to dobře chápu. Tohle si určitě uplatnění najde.
PS: zajištění validace pointerů to mě pravda nenapadlo. Já si tam ukládám offsety, protože pointery u vektoru zaručené nejsou, to je fakt. Jinak pokud něco takového potřebuju dnes, tak používám deque s tím, že si v položce dávám nějaký bit, který označuje, zda je pozice obsazena nebo není. Lifetime lze řídi v constexpr přes union {}
Jen ještě k těm verzím gcc. Tyto základní překladače jsou typicky použity pro překlad systému, ale normáně jde z oficiálních repozitářů dané distribuce nainstalovat novější verze. Ubuntu LTS teda neznám, takže tam to je možná jinak.
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 57 649×
Přečteno 27 729×
Přečteno 26 407×
Přečteno 24 371×
Přečteno 22 874×