Can I avoid classes in this situation?

I try my best to follow the KISS and YAGNI principles and often times I try to see how I can avoid classes since I have a bad habit at OOPifying what I do and it typically adds unnecessary complexity. I’m working on a fighting game with a handful of playable characters with their own actions and thinking about how easy it would be for a frontend dev to create these actions I’ve opted for an OOP approach where the base class provides easy access to lower level systems and the developer would (or should) only have to focus on the derived class within some ability function. The question I have is this the “easiest” way to do this or can it be rewritten without using classes?

function BaseCharacter.new(characterData)
	local self = setmetatable({},BaseCharacter)
	self.data = characterData
	return self
end

function BaseCharacter:PlayAnimation()
	print("BaseCharacter:PlayAnimation()")
end

function BaseCharacter:PlaySound()
	print("BaseCharacter:PlaySound()")
end

function BaseCharacter:SpawnParticles()
	print("BaseCharacter:SpawnParticles()")
end
function DerivedCharacter.new()
	local self = setmetatable(BaseCharacter.new(), DerivedCharacter)
	return self
end

-- Someone creating an ability works within this function using the API from the base class
function DerivedCharacter:Execute()
	self:PlayAnimation()
	self:PlaySound()
	self:SpawnParticles()
end
1 Like

Older styled games (before OOP was so popular) used mega tables to define classes. A template of a base class (health and stamina I suppose) and then an additional template of whatever other class the player chooses. Witch could add magic, vampire could add immortality, etc.

You can do this with classes, and it makes sense in my eyes to do so, but then you lose out on the idea of mixing classes. Ie gamepass for 2 classes at once (witch and vampire).

So some sort of a mega template and then an identifier that links the player to its template (class). You can use attributes for any unique player data like stamina/mana.

I used examples here but the idea is solid, that you can avoid OOP by just avoiding OOP. Use tables, use lots of elseifs, and use attributes/values.

1 Like

It can be rewritten without classes. Should you do it, though? Maybe. OOP is what most people are used to in Luau, and that may be the case for you too. If you feel like you could most productive with it, then go ahead. But, if you want an alternative anyway, you could go with procedural.

The main difference between procedural and object-oriented (IN THIS CASE) is that a class’ implementation is kept separate from their definition. Consequently, we can no longer use : syntax to call member methods. We have to explicitly pass the object as an input, whereas self would’ve previously been inferred.

type Animal = {
    age: number,
    name: string?
}

type Cat = Animal & {
	evil: false
}

type Dog = Animal & {
	good: true	
}

local AnimalImpl = {}

function AnimalImpl.printName(animal: Animal)
	print(`name: {animal.name}`)
end

local CatImpl = {}

function CatImpl.new(age: number, name: string?): Cat
	return { 
		age = age, 
		name = name, 
		evil = true
	}
end

local DogImpl = {}

function DogImpl.new(age: number, name: string?): Dog
	return { 
		age = age, 
		name = name, 
		good = true 
	}
end

local myCat = CatImpl.new(10, "Foobaz")
local myDoggo = DogImpl.new(20, "Foobar")

AnimalImpl.printName(myCat) -- because Cat: Animal
AnimalImpl.printName(myDoggo) -- because Dog: Animal

I think it is useful to think of Animal as being a reusable trait / a base class, and Cat and Dog being concrete classes.

A benefit I could mention is the more optimized packing of data, and one less indirection (because of __index).

A con would be that you would have to manually implement the Animal trait, which would unfortunately trigger the DRY event.

what do the readers think about this implementation?

2 Likes

Not useful commentary but ig the “You’ll either use OOP for the rest of your life or see yourself slowly returning back to functional” is true lol

1 Like