Classes and QuakeC ================== By Marco Hladik Last updated: 8th of August 2018 Introduction ============ Object Oriented Programming makes a lot of sense for game-logic. Especially in an environment like QuakeC, where the language already attempts an introduction of datatypes (in this case `entity`) that are meant to represent "objects". However, the entity type has its downsides once you attempt to scale the codebase. The entity type in QuakeC uses something called `fields` to give them their attributes. self.health = 100; Sets the field `.health` of entity `self` to 100. .health is just one of many fields used in the stock QuakeC codebase. There are a set of fields that are a requirement to have, like .origin and .angles, as they're networked by default so that the engine knows how to render said entity. Some other fields like .weapon and .items as well as .armor are specific to Quake 1 only. One thing that always annoyed me about regular entities in QuakeC is that every field was globally declared for _every_ other entity in the game. So you end up with thousands of fields (in the case of Xonotic) that are being allocated for the entities. Thousands of references, most of the time not going anywhere. This leads to some programmers committing something titled `field reuse`. Suddenly you'll see people use one field (like .health) to store info about something completely unrelated to what the name implies. Each field costs each entity at least 4 bytes of memory. Vectors are worse, because they're 3 floats bundled together. So that's at least 12 bytes on every entity spawned - even when unused. Anyway, classes and FTEQCC solve this problem somewhat. Classes are, technically, by extension, entities. They inherit all the global, standard fields any other entity gets. So your object of class "CFunBalloon" will have all of the globally defined fields .origin, .angles as well as .modelindex available. Classes can also be referenced as entities and manipulated: CFunBallon tommy = spawn( CFunBallon ); entity tommyreference = (entity)tommy; setorigin( tommyreference, '0 0 0' ); // Perfectly valid This also works in reverse. If you know that the entity type being returned is of a specific class, you can cast it to said class and manipulate its exclusive fields. The compiler is fine with it. Basic Declaration ================= Here it is: class CMyClass { }; This officially declares a class, titled CMyClass. It has no attributes, other than the ones it inherits from all entities (aka all fields). It also has no methods associated with it. You don't have to call it C**** anything. You can also call it Bob or Miguel for all the compiler cares. That's just how most programmers, coming from other languages would declare it. Attributes/Members/Fields ========================= Every language calls them differently. I don't think there's an official preference on how to call it in QuakeC, so I'll just call them attributes. Now since we've got our CMyClass, we want to add an attribute to it. One that no other class or entity can use. class CMyClass { int myattribute; }; Some languages have public/private declarations for attributes and methods. QuakeC doesn't care. Let's move on. Spawning a class ================ A class cannot just be defined and then manipulated. Just like an entity reference, it has to be spawned first. spawn() will take care of that. CMyClass tom = spawn( CMyClass ); spawn() for standard entity types does not take a parameter, however it's a requirement here. The spawn() will also tell it to launch the constructor. Which we'll go into next. Map entities can also directly spawn and be initialized as a class instead of a function. Constructor =========== The constructor needs to be defined first inside the class description: class CMyClass { int myattribute; void() CMyClass; // The constructor declaration }; Upon spawning the class with spawn(), it'll look for the member method titled after said class. // The constructor definition void CMyClass :: CMyClass ( void ) { if ( !myattribute ) myattribute = 100; } Inside this example constructor, it'll define myattribute to be 100 when created via spawn() and myattribute has no value. It's also possible to modify the attributes inside of an object with the spawn() intrinsic before the constructor is called: CMyClass tom = spawn( CMyClass, myattribute: 255, model: "Tom" ); Will set the myattribute to 255. Which will prevent the constructor from re-defining it to 100 due to the `if (!myattribute)` in the constructor. It'll also set the `model` field it inherits from entities to "Tom". Just to give you an example of what defining multiple attributes looks like when using spawn(). Methods ======= Let's create a method titled 'Destruct'. This will teach more than one thing in particular. Declaration (shove that into your class description somewhere): virtual void() Destruct; Definition: void CMyClass :: Destruct ( void ) { remove( this ); } `this` effectively acts like `self`, when referring to which object is calling its member function. This also changes the way you can call member functions: entity oldself = self; self = other_entity; self.FooBar(); self = oldself; Can now be replaced comfortably with: myobject.FooBar(); Because the compiler knows that if FooBar() is a method of a class that you'll be wanting to execute it on `myobject` and not whatever `self` is set to. In Stock QuakeC you always have to beware what `self` is set to when calling entity functions. Not here. Conversion ========== It is indeed possible to convert an already spawned, existing entity. This means going from entity to an object of a class. For example, in Server-Side QuakeC there's the function PutClientInServer. If you were to put the following line in there: spawnfunc_CMyClass(); It would convert the already spawned player entity into an object of the class CMyClass. Inheritance =========== One of the biggest pros of classes in my opinion. We all know that CMyClass has already inherited all the standard `entity` type fields. However, FTEQCC supports single inheritance. class CCoolestClass : CMyClass { int newattribute; void() CCoolestClass; }; void CCoolestClass :: CCoolestClass ( void ) { myattribute = 666; newattribute = 123; Destruct(); } So now CCoolestClass inherited all of CMyClass and the basic `entity` type. This allows an unprecedented amount of complexity conveyed in very few pieces of code. Remember that if you override a method of a class you inherit, that you have to declare that as such inside the class definition. Problems/Workarounds ==================== As you noticed in the Chapter for Methods, we can set fields without specifically notating `this.` before setting `myattribute`. This results in functions like e.g. find() to complain if you were to call it like this inside a member method: blargh = find( world, netname, "Tom" ); // error It will tell you about 'netname' not being defined. The compiler thinks that netname is a member of your class whose value you want to pass as a parameter and not a type of field/attribute reference. Adding a :: before netname will tell the compiler that you're looking for a field/attribute. blargh = find( world, ::netname, "Tom" ); // valid