Mechanics hobby RTS + start C++ implementatie

Door Gamebuster op maandag 13 april 2015 19:09 - Reacties (6)
Categorie: Hobby RTS, Views: 3.457

C++ coders, code review is welkom:
https://github.com/tobyhinloopen/My-CPP-RTS



De Hobby RTS waar ik al enkele artikeltjes over heb geschreven is nog steeds hard in ontwikkeling.

Om te beginnen, de RTS heeft o.a. energie en massa als resources. Er is geen globale opslag van resources. Energie kan draadloos gedeeld worden, moet gegenereerd worden met Massa of moet opgevangen worden met zonnecellen. Massa moet fysiek opgenomen, opgehaald, gedeeld of overgebracht worden.

Ik werk nu de mechanics uit. De "droom" van de game is groot, 1 significant deel van de "droom" is de mogelijkheid om je eigen units te ontwerpen. Hierbij bepaalt de uitrusting - de geselecteerde componenten - van de unit de eigenschappen van de unit.

Een unit begint volledig niet-bestaand. Er is geen "basis-unit" of frame; je begint met niets. Iedere feature die een unit kan hebben, moet je zelf toevoegen. Stel, we willen een simpel gevechtsvliegtuig maken.

Je gevechtsvliegtuig begin je dan bijv. met het geven van een frame. Dit frame is puur visueel en heeft geen invloed op de eigenschappen van de unit.

Voor een gevechtsvliegtuig heb je uiteraard wapens nodig. Als speler beschik je uit een set componenten, waaronder enkele wapens. We geven het vliegtuigje 2 laserwapens.

De toegevoegde laserwapens hebben stroom nodig om te schieten. Voor de stroom voegen we een power generator toe; deze zet een zeer kleine hoeveelheid mass om in stroom. De mass die nodig is om omgezet te worden wordt opgeslagen in een mass storage, die ook toegevoegd wordt aan de unit.

Onze unit kan nu schieten, mits er Mass opgeslagen is in de MassStorage.

De unit kan zich echter nog niet verplaatsen. Hiervoor voegen we een kleine verbrandingsmotor toe. Deze verbrandingsmotor verbruikt massa en zet deze om in beweging. De massa is afkomstig van de mass storage die we eerder al hebben toegevoegd. Uiteraard moet er wel massa aanwezig zijn in de storage; de MassStorage kan dus opraken, waarna de unit zonder wapens of motor zit. Bij het maken van de unit kan de mass storage ook meteen gevuld worden, maar dit is niet vereist (instelbaar)

Tot slot voegen we armor toe aan de unit. Per eenheid "armor" neemt de armor-eigenschap toe van de unit, waardoor de andere onderdelen beschermd zijn tegen damage. Hoe het toevoegen van armor visueel eruit zal zien, weet ik nog niet. Wel weet ik al dat de visuele indeling van units geen impact hebben op de eigenschappen van de unit. De indeling is puur visueel. De unit eigenschappen worden gebaseerd op puur de aanwezigheid van componenten, niet de positionering ervan.

Onze unit bestaat nu uit:

20x Armor
MassStorage (kleine opslag voor massa)
PowerGenerator (zet zeer kleine hoeveelheden massa om in energie)
LaserWeapon (x2, verbruikt energie per schot)
CombustionEngine (verbruikt mass)

Alle onderdelen die toegevoegd zijn hebben o.a. de volgende basis eigenschappen:

Shield
Wanneer een component een shield heeft, voegt deze shield-armor toe aan de unit. Shield beschermt de componenten tegen inkomende schade en kan kosteloos herladen worden. Verbruikt energie om online te houden en kan tegen extra energieverbruik zich herstellen tot maximale capaciteit na schade.

Armor
Armor beschermt de componenten wanneer er geen shield (meer) is. Bij schade verliest de armor zijn massa tot deze 0 is en de armor geen bescherming meer biedt. Reparatie van armor is alleen mogelijk door weer massa toe te voegen met speciale componenten bedoelt voor reparatie of constructie.

Health
Wanneer shield en armor geen bescherming meer bieden, blijft de health van een component over. De health is vaak laag; de componenten met de minste health zullen het eerste uitschakelen of zelfs exploderen wanneer deze van nature instabiel zijn. Reparatie van componenten die beschadigd zijn kan alleen gedaan worden door constructie units en verbruikt meer energie dan de oorspronkelijke constructie van de componenten.

Volatility
Sommige onderdelen zijn van nature explosief, zoals accus, generatoren en motoren. Wanneer de onderdelen beschadigd raken, kunnen ze dus schade toebrengen aan andere onderdelen in de unit en/of omliggende units. Volatility kan misbruikt worden voor suicide units.

Complexiteit
Bepaalt hoeveel energie en beïnvloed de hoeveelheid tijd er nodig is om de component te maken of te repareren.

Massa
Bepaalt het gewicht van de unit, wat nadelig werkt op de behendigheid en snelheid van de unit. De massa is ook nodig bij het maken van de unit. Voor een Mass Storage telt de opgeslagen massa mee met het gewicht van de unit.

Het verbruik en productie van resources per onderdeel is ook vastgelegd. Dit kan bijv. zijn:


code:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Laser, in gebruik, per stuk:
  power_consumption: 40

Power Generator, in gebruik
  mass_consumption: 0.1
  energy_production: 60
  mass_capacity: 10
  energy_capacity: 240

Mass Storage
  mass_capacity: 40

CombustionEngine, in gebruik (vol gas)
  mass_consumption: 0.1



In detail:

De bovengenoemde onderdelen kunnen in totaal dus 50 massa opslaan. De generator kan zelf 240 energie opslaan en per seconde 60 genereren. Zodra energie "verbruikt" wordt, wordt dit opgenomen uit die 240. Zolang de energie opslag niet vol is, blijft er energie geproduceerd worden. Een lege accu kan in 4 seconden gevuld worden.

Met de in totaal 50 massa die opgeslagen kan zijn in de unit, kan de accu bijgeladen worden met 60 per seconde voor 50/0.1=500 seconden. Er kan dus in totaal 60*500=30.000 energie gegenereerd worden met een volle massa opslag.

Met 30.000 energie kunnen kan 1 laser 750 seconden lang schieten. Omdat er maximaal 60 energie per seconde geproduceerd kan worden en er 80 energie per seconde verbruikt wordt bij het afvuren van beide lasers, is er 20 energie per seconde nodig uit de accu voor het in stand houden van het vuren van de lasers. Dit houdt in dat de 2 lasers maximaal 240/20=12 seconden op vol vermogen kunnen vuren. Daarna neemt de fire-rate af tot 60/80=75%.

De bovenstaande situatie gaat er echter van uit dat de unit zich nooit verplaatst. De verbrandingsmotor verbruikt ook massa. Wanneer de unit nooit schiet, kan de unit op vol vermogen vliegen voor 50/0.1=500 seconden.

Wanneer de opslag leeg is, moet deze bijgeladen worden. Dit kan bij units die beschikken over opgeslagen massa en een component die mass transfer mogelijk maakt. O.a. de fabrieks-unit waar de unit in gemaakt is, heeft de mogelijkheid om de unit weer "bij te vullen". Tevens kan de fabrieks-unit ook beschadigde armor en componenten repareren. Je kan je unit ook demonteren bij de fabrieks-unit om de massa in de unit componenten weer op te nemen.

De game zal veel draaien rondom het verzamelen en beheren van Massa. Energie is makkelijk te verkrijgen en te beheren. Massa is zeldzaam en "uitdagend" te beheren. Omdat je energie kan verkrijgen uit massa, zullen de meeste units een energy generator en een mass storage aan boord hebben.

Massa zal nooit verloren gaan in een game. Bij het maken van een unit gaat de massa uit de fabrieks-unit's mass storages naar de componenten van de nieuwe unit. Alle massa blijft in die unit tot deze gedemonteerd wordt. Zelfs wanneer een unit volledig vernietigd is, kan de massa herbruikt worden. In enkele situaties kan massa van de units af gaan, door verbranding of vernietiging van bepaalde componenten. In dit geval zal de "verloren" massa "opgenomen" worden door de wereld, klaar om opnieuw verzameld te worden.

Volgende: Shoarmabroodje kant & klaar van Albert Heijn 05-'15 Shoarmabroodje kant & klaar van Albert Heijn
Volgende: Neem eens 7 minuten en 19 seconden rust. 04-'15 Neem eens 7 minuten en 19 seconden rust.

Reacties


Door Tweakers user Jogai, maandag 13 april 2015 19:38

De massa wordt omgezet in energie bij bij het schieten en transporteren. Dan gaat massa toch verloren? Of krijg je dan zure regen die je kan minen oid?

Lijkt een leuk projectje!

Door Tweakers user Gamebuster, maandag 13 april 2015 22:04

Massa in transport is opgeslagen in de transporter. "Verloren" massa komt inderdaad weer terug in de omgeving

Door Tweakers user H!GHGuY, dinsdag 14 april 2015 10:14

Man, man... nu ook al in de vrije tijd code review doen :+

Bedenk even wat methodes zijn... Het zijn acties. Acties worden in de taalkunde gedefinieerd door werkwoorden... Nochtans heeft geen enkele van de methodes die ik heb gezien een werkwoord in de naam.

Typische naamgevingen zijn:
IsDead(), IsAlive(),
GetHealth()
Force::IsNone(), IsAny()

Het is ook mooi dat je bvb een none() en any() voorziet (waarbij any() = !none()), maar dat is eigenlijk zinloos. Laat de gebruiker van je class kiezen.
Maar... Welke doe je weg?
Aangezien de kunst in het coderen eigenlijk psychologie is, doe je de none() en dead() weg. Reden? Mensen kunnen moeilijk om met dubbele negaties: !none() of !dead() leest moeilijker dan IsAny() of IsAlive().

De structuur van je code is ook nogal adhoc... Kijk even naar wat typische opensource projecten. Zomaar alles in je project root gooien is niet echt proper ;)


code:
1
UnitMotionManager::UnitMotionManager(const shared_ptr<const vector<shared_ptr<Component>>> components)


Say what?
een const shared pointer naar
--> een const vector met daarin
------> mutable shared pointers naar
------------->mutable components...

Iets vertelt me dat je data model niet echt zal schalen... Heb je geen nood aan een soort componentgroup die dan ook full ownership heeft van de child components?
Zou het dan voor scalability ook niet beter zijn dat components in een componentgroup hun taken delegeren aan de group? Of beter gezegd: dat wanneer een component in een group gestopt wordt, hij plots een soort 'dumb' component wordt?

UnitTemplate::components()
Bedenk dat je hier eigenlijk een factory pattern hebt gemaakt. Wat doen ze in een factory? Create()'en dus ;)

Ik ben zelf geen game developer, maar weet wel dat game development een soort 'aparte' tak is in de software industrie, wanneer het op bepaalde design patterns aankomt.
Zie ook:
http://gameprogrammingpatterns.com/
http://cowboyprogramming..../05/evolve-your-heirachy/

Door Tweakers user Gamebuster, dinsdag 14 april 2015 11:29

Heel erg bedankt voor je feedback.

De naming conventies kan ik jaren over discussieren. Ik heb gekozen voor de huidige stijl en ben hierin consequent binnen dit project. Tevens koos ik ervoor om deze stijl aan te houden en niet een specifiek andere (o.a. jouw capitalized camelcase stijl) omdat er eigenlijk geen leidende stijlguide lijkt te zijn in C++ land. Mijn stijl is afkomstig van mijn ervaring met Ruby en Javascript.

De dubbele helper-methods dragen naar mijn mening bij aan de leesbaarheid vanuit de gebruiker van classes. Zo vind ik het lezen van
code:
1
if(something.dead)

prettiger dan
code:
1
if(!something.alive())

. Beiden zijn aanzienlijk beter dan
code:
1
if(something.health() == 0)

.

Een gebrek aan is_ voor boolean accessors heb ik voor gekozen omdat de aanwezigheid van is_ niet toevoegt aan de leesbaarheid, maar de namen wel langer maakt: Ik hou van zo kort mogelijke method names, maar dan zonder gebruik van afkortingen.

Mijn mooie shared_ptr<vector<shared_ptr<>>>'s vind ik inderdaad een beetje eigenaardig, maar ik weet niet echt wat een betere keuze zou zijn. Suggesties zijn welkom.

Mijn datamodel zal ik zeer waarschijnlijk aanpassen wanneer ik ervaar dat dit nodig gaat zijn. De test-driven design en mijn gebrek aan ervaring op dit gebied zorgen ervoor dat ik dit soort keuzes (het groeperen van componenten e.d.) zal uitstellen tot ik concreet een beeld heb over hoe ik dit het beste kan doen. Voor nu werkt de "platte" aanpak prima. Bedenk ook dat componenten vaak meerdere rollen kunnen vervullen en het daarom lastig is om deze in hokjes te stoppen.

Het hernoemen van UnitTemplate::components naar ::create_components vind ik goede keuze en legt nadruk erop dat components() geen getter is maar een factory.

Leuke links trouwens! Daar ga ik eens lezen

[Reactie gewijzigd op dinsdag 14 april 2015 12:00]


Door unknown, dinsdag 21 april 2015 18:28


code:
1
using namespace std;



No! http://stackoverflow.com/...d-considered-bad-practice


code:
1
const shared_ptr<const vector<shared_ptr<Component>>> components



Typedef? :


code:
1
2
3
typedef std::shared_ptr<Component> ComponentPtr;
typedef std::vector<ComponentPtr> CompontentPtrList;
typedef std::shared_ptr<ComponentPtrList> ComponentPtrListPtr



Nu zie je zelf hoop ik ook wel in hoe gek dat is. Waarom heb je uberhaubt een vector van shared pointers naar Component instances? Waarom niet gewoon:


code:
1
2
3
4
5
6
std::vector<Component>[code]

Als de copies die je de vector in douwt dan als nog:

[code]typedef std::shared_ptr<Component> ComponentPtr
std::vector<ComponentPtr>



Waarom zou je de hele tijd een shared pointer naar een vector willen door geven? Is een const reference niet genoeg?

Dan nog je member variable. Je maakt kwa naamgeving geen onderscheidt tussen class member variables en local variables.

Het is in (modern) C++ gebruikelijk om voor member variable iets in deze trant te gebruiken:


code:
1
int m_ding;



Of zoals in veel moderne code bases (zie Google's style guide):


code:
1
2
int ding_;
double ander_ding_;



Zo kun je als je de code leest snel de (private/protected) member variable identificieren.

Dan, zoals ook door iemand anders al genoemd. Het is gebruikelijk om functies te laten beginnen met een werkwoord. Dus:


code:
1
2
is_alive()
is_dead()



Jij geeft als argument aan dat het 'is_' stukje niet zo veel toevoegt. Daar ben ik het niet mee eens. Als ik dit lees:


code:
1
if(alive())



Moet ik drie keer nadenken (overdrijven is een vak) voor dat ik snap dat je eigenlijk bedoeld 'als X levend is'.

Terwijl:

[code]if(is_alive())[/code}

Dead obvious is.

Door Tweakers user Gamebuster, dinsdag 21 april 2015 18:47

Ten eerste, heel erg bedankt voor het doorlezen van de code om hierop feedback te geven. Dit kan ik altijd waarderen :)
Using namespace std is bad practise
Kan ik me in vinden en deze hoor ik vaker. Ik ga overwegen deze weg te werken. In combinatie met je typedefs kan ik de hoeveelheid std::'s al significant verminderen.
vectors met daarin shared pointers naar componenten
Wat ik wil bij die vectors met componenten erin is dat ik een reference naar een enkel item in die vector wil kunnen bewaren. Ik wil dat als ik een waarde update in het item in de vector, dat ik deze kan uitlezen via die reference. Ik koos er destijds voor om shared_ptr's te gebruiken omdat die bleken te werken.

Ik ga die shared_ptrs wegwerken bij de eerst volgende refactor sessie en uitzoeken hoe ik referenties ernaar kan maken.
de is_ geen is_ discussie
Wanneer ik gebruik maak van underscore naming, gebruik ik geen is_ prefixes. Dit maakt de functienamen te lang en ik vind het niet nodig. Dit is echt een kwestie van smaak en gewenning. De makers van de C++ STD lib kunnen zich vinden in mijn keuze: Die gebruiken ook geen is_:

http://www.cplusplus.com/reference/set/set/empty/
http://www.cplusplus.com/reference/thread/thread/joinable/

Mijn keuze hierin staat vast: Ik gebruik geen is_ prefix.

Wel valt me op dat de C++ std niet consistent lijkt te zijn; Soms wordt er voor "getters" wel een get_ prefix gebruikt, terwijl andere keren niet. De vraag of ik zelf get_ en set_ prefixes gebruik of niet staat nog open. Zie bijv.:

http://www.cplusplus.com/...ead/thread/native_handle/
http://www.cplusplus.com/reference/thread/thread/get_id/
Private variable names
Dit blijft een lastige. Ik vind persoonlijk een _ of m_ prefix of _ postfix lelijk. Tegelijkertijd kom je er niet onderuit als je methodes hebt met dezelfde naam als een private variable. Stel, je hebt x als private variable, dan kan je niet ook een methode x maken. Dan moet je dus iets doen met die variable naam of methode naam.

De ene manier zou in mijn ogen zijn om inderdaad een variable prefix toe te voegen, maar dit vind ik slordig. De andere manier is de methode naam veranderen naar get_x en set_x. Ook dit vind ik niet super.

Voor nu kies ik de underscore prefix voor private variables, maar ik ben er nog niet over uit.

Uiteindelijk wil ik eigenlijk proberen bij de C++ std lib coding guidelines te blijven. Mijn classnames kloppen dan al niet; deze ga ik nog omzetten.

[Reactie gewijzigd op dinsdag 21 april 2015 18:51]


Reageren is niet meer mogelijk