Recent

Author Topic: What is the best/versatile/efficient etc way to describe game character?  (Read 6716 times)

TomTom

  • Full Member
  • ***
  • Posts: 170
I dont' know if I'm asking good question :S but...
Finally I've decided to start (slowly) creating this simple maze-dungeon-rogue-rpg-adventure-like game. Nothing fancy, just random dungeon, player character and few monsters to slay. What I've done so far is ... almost nothing. I want to make small steps, so  I've written code which generates random dungeon. It works and it's quite easy.
My second step was to create player character and give it possibility to move around that dungeon. It works.
I did that by:
- using Array of string for Dungeon ('#' is wall and ' ' [space] is ... empty space)
- player character is
Code: Pascal  [Select][+][-]
  1. type
  2.   TPlayer = class
  3.     private
  4.       FPosX : Integer; //position X in dungeon
  5.       FPosY : Integer; //position Y in dungeon
  6.       FPRect: TRect; //TRect for player
  7.  
  8.       property PosX: Integer read FPosX write FPosX;
  9.       property PosY: Integer read FPosY write FPosY;
  10.       property PRect: TRect read FPRect write FPRect;
  11.  
  12.     public
  13.  
  14.   end;
  15.  

But I'm wondering if its good to do this like this.

I'm thinking about
- Creating class for every living entity in world with info about
  - FEntityType : etNPC, etEnemy, etPlayer
  - FEntityClass : ecWarrior, ecWizard, ecRogue, ecCleric etc etc.
  - FEntityHealth: integer;
  - FEntityAttitude : eaHostile, eaFriendly, etNone = for player?
  - FEntityStr : integer; //Attack strength of entity?
  - FIsQuestGiver: boolean; // I don't know if there will be quests, maybe except slay 10 rats
  - FEntityRace: erHuman, erOgre, erDwarf etc
  - FEntityRect: TRect // for image to Display
  - FPosX: integer;
  - FPosY: integer;
etc etc etc

and then create sth like this

TPlayer = class(TEntity)
TEnemy = class(TEntity)
 etc etc etc

Does it make any sense? Or am I thinking all wrong about this.

 

Handoko

  • Hero Member
  • *****
  • Posts: 5154
  • My goal: build my own game engine using Lazarus
I'm not an expert in OOP. But I will do something like this:

1. Use the power of OOP - subclassing

TEntity, subclass to:
- TStaticObject
- TMovingObject

TMovingObject, subclass to:
- TNPC
- TPlayer

FEntityType, FEntityClass, FEntityRace can be omitted if you subclass them properly.

2. TEntity has these fields:

- FPosX
- FPosY
- FWidth
- FHeight
- FAnimImage
- FAnimProgress
- FAnimDelay

The FAnim* are used for doing animation. How to use it you can see the source code of FuriousPaladin I wrote.

Note:
You maybe want to try Mazer - procedural maze dungeon in 3D.
https://forum.lazarus.freepascal.org/index.php/topic,29543.msg186811.html#msg186811

TomTom

  • Full Member
  • ***
  • Posts: 170
Thanks Handoko :) I don't know if I understand it right (You wrote 'subclass to' but shouldn't it be 'subclass OF' ?):

Code: Pascal  [Select][+][-]
  1. TEntity (BASE OF ALL)
  2.     |
  3.     |____ TStaticObject (walls?)
  4.     |____ TMovingObject (player, monsters)
  5.              |
  6.              |______ TMonster
  7.              |______ TPlayer


Why EntityType, EntityClass and EntityRace could be omitted? Im my case both Monster and Player could have same class types (warrior, mage etc), so I guess it would be convinient if TPlayer and Tmonster inherit those fields from Base (TEntity) class.

I'm not planning to do any animations for this project. This already is complicated to me and hard to understand without animations :P



lucamar

  • Hero Member
  • *****
  • Posts: 4219
As in all object hierarchies, you have to abstract common behaviour from specific ones. For example, your mages can launch spells, a warrior can have his weapons, etc. But both of them have certain characteristics in common: they can move through the maze, they have some health, etc.

These later characteristics, common to all type of players could go to the TGameCharacter class while characteristics proper to each kind would go into a child class like: TWarrior = class(TPlayer).

In your program then you can differentiate between each player by using for example:
Code: Pascal  [Select][+][-]
  1. if Player is TWarrior then ...
so you wouldn't need to store the kind in the general TGameCharacter class: each child class already knows what it is.

Since this is valid for almost every entity in your game: enemies, plunder, etc. you create a base class TEntity with the bare minimum info & methods needed: size, position, maybe its shape (whether as a char or a bitmap), etc. and from this base class you derive one child for static objects like treasures, emergency packs, etc. which can't move themselves but can change position (by a dynamic "object" carying them, p.e.) and dynamic ones like "monsters" and players.than can move by themselves.

That's basically how it could be done.
« Last Edit: March 07, 2019, 09:13:48 am by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

TomTom

  • Full Member
  • ***
  • Posts: 170
And that's how I was thinking about this (more or less).

As in all object hierarchies, you have to abstract common behaviour from specific ones. For example, your mages can launch spells, a warrior can have his weapons, etc. But both of them have certain characteristics in common: they can move through the maze, they have some health, etc.

These later characteristics, common to all type of players could go to the TPlayer class while characteristics proper to each kind would go into a child class like: TWarrior = class(TPlayer).

In your program then you can differentiate between each player by using for example:
Code: Pascal  [Select][+][-]
  1. if Player is TWarrior then ...
so you wouldn't need to store the kind in the general TPlayer class: each child class already knows what it is.

Since this is valid for almost every entity in your game: enemies, plunder, etc. you create a base class TEntity with the bare minimum info & methods needed: size, position, maybe its shape (whether as a char or a bitmap), etc. and from this base class you derive one child for static objects like treasures, emergency packs, etc. which can't move themselves but can change position (by a dynamic "object" carying them, p.e.) and dynamic ones like "monsters" and players.than can move by themselves.

That's basically how it could be done.

lucamar

  • Hero Member
  • *****
  • Posts: 4219
And that's how I was thinking about this (more or less).

And as always, the devil is in the details :D

I don't do it much but when I've to I sit down with paper and pencil and start joting done what each final class needs. After that, I start abstracting up until I reach the base class, whether really "base" or an existing one. Usually it works, though sometimes it requires quite a few changes of point of view ;D
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

TomTom

  • Full Member
  • ***
  • Posts: 170
@lucamar
Now I'm on that stage. Planning with pen and paper :), and I came across many things that are hm... foggy to me :) At first they seem to be good and in a second I'm discovering possible problems ... That's hard, so that's why I asked.

And BTW if I would need that in future. How can I keep track of for example TOgre(TEnemy)  instances?
Code: Pascal  [Select][+][-]
  1. For i := 0 to EVERY_TOgre - 1 do
  2. Begin
  3. TOgre[i].SetStrenght(123);
  4. end;

I would need to store them somewhere hm.. but where? Maybe Array [0..MAX_MONSTER_COUNT] of TMonster ... or sth like that...



« Last Edit: March 07, 2019, 11:18:59 am by TomTom »

marcov

  • Administrator
  • Hero Member
  • *
  • Posts: 11453
  • FPC developer.
In many games with a large playfield, the movement is recorded also by the place of the player in the quadtree. (a datastructure that allows to quickly find nearby other players and objects).

Modelling movement with an inheritance tree might therefore be a bit simplistic.

Handoko

  • Hero Member
  • *****
  • Posts: 5154
  • My goal: build my own game engine using Lazarus
Maybe Array [0..MAX_MONSTER_COUNT] of TMonster ... or sth like that...

Using array usually faster than TList. But I prefer TList or TFPList, which are easier when handling new objects spawning or removing (killed).

In my snake game tutorial, I explained how to modify code from array to use TList:
https://forum.lazarus.freepascal.org/index.php/topic,38136.msg260867.html#msg260867

Im my case both Monster and Player could have same class types (warrior, mage etc), so I guess it would be convinient if TPlayer and Tmonster inherit those fields from Base (TEntity) class.

I see, you want to have overlapping behaviors but in different classes. Then it will become more complicated. If it is for learning purpose, I recommend you to write the code now without making the monsters to have same class types as the player.

If you're planing a great game, or commercial game, or after you have some understanding about how to properly use the power of OOP, then you should do the planning in detail before you start to write any code.

Planning is very important especially for big projects or commercial projects, it will save a lot of efforts for future maintenance and development . But over planning without understanding what you're doing is bad. You've just learned something about planning, build the game now. But next time, plan it again but in more detailed.
« Last Edit: March 07, 2019, 12:23:07 pm by Handoko »

TomTom

  • Full Member
  • ***
  • Posts: 170
@Handoko
Sorry but I need to ask this (I know it may sound stupid but this is all because language barriers :P ). What are You saying is:

Create Class for TPlayer with Movement, Statistics fields (Strength, Health etc)., race
Create Class for TOgre  with all above except race maybe 
Create Class for TGoblin with all above except race maybe
Create Class for TAnyKindOfEnemyOnGame with all above...

and just go with that for now.

Handoko

  • Hero Member
  • *****
  • Posts: 5154
  • My goal: build my own game engine using Lazarus
Maybe this can make you better understand what I meant:

TMovingObject
           |
           |__ TPlayer
           |           |
           |           |__ TWarrior
           |           |__ TMage
           |           |__ TBard
           |
           |__ TMonster
                       |
                       |__ TOgre
                       |__ TGoblin
                       |__ TTroll


TMovingObject has these properties:
- FHealth
- FLevel
- FWeapon = TItem
- FHandicap = set of (hdInjured, hdBleeding, hdStunned)

TMonster has these properties:
- FAggression = (agKind, taNormal, taAttack)
- FAction = (acIdle, acWalking, acAttacking)
- FMovement = (mvUp, mvDown, mvLeft, mvRight)
- FItemsToDrop = list of TItem

TOgre has these property:
- FBarbaricMode: Boolean

TGoblin has these properties:
- FMagicUser: Boolean
- FSpell: TSpell

TTroll has these properties:
- FFrightened: Boolean

TPlayer has these properties:
- FInventory = list of TItem

TWarrior has these properties:
- FEnraged: Boolean

TMage has these properties:
- FMana
- FSpellLearned = set of TSpell

TBard has these properties
- FLuck

Each class has their own unique behavior, like this:
  • Bard can cast spells but only using the scrolls in the inventory. Mage can learn new spells (from scrolls found) and cast spells using his mana or scrolls.
  • Bard has luck, which increases the chances for successful attack and opening locks.
  • On certain conditions, warrior can be enraged and do double damage.
  • When injured, it will decrease the chances for successful attack.
  • When bleeding, the health will drop slowly until a period of time or being healed.
  • When stunned, it can't move or attack for a period of time.
  • Activate barbaric mode increase the damage it can cause and receive only half of damage.
  • Some goblins are magic user.
  • Trolls are afraid with lightning and fire, which make them to run away ignoring its current action.

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Depends on the game, of course, but I would reduce that hierarchy more or less:
Code: [Select]
TGameCharacter
           |
           |__ TWarrior
           |__ TMage
           |__ TBard
           |__ TOgre
           |__ TGoblin
           |__ TTroll
All characters, whether players or monsters have common properties that should go into their base class but the distinction of "player" vs. "monster" doesn't seems very meaningfull. Note that most of what you use to distinguish them really pertains to both:  Wouldn't a bard have a lower level of agression than a warrior? And what if an enemy is a mage and a goblin is a friend? :)

I would then build a TPlayer class with properties and methods for the human player itself: kind of personage (a TGameCharacter), score, etc.

Each to it own, I suppose, but it seems more logical: shallow hierarchies are always easier to maintain.
« Last Edit: March 07, 2019, 04:56:29 pm by lucamar »
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

Handoko

  • Hero Member
  • *****
  • Posts: 5154
  • My goal: build my own game engine using Lazarus
I separate TPlayer and TMonster because TPlayer has code for handling user input and TMonster has code for AI. Also TPlayer can gain experience and level up, TMonster are fixed in the level when they're spawned.

But thing can be different. If user can play as a troll or ogre, then yes they (TWarrior .. TTroll) should be on the same ancestor.

That's just my suggestion for the design. No need to follow exactly as what I said.

lucamar

  • Hero Member
  • *****
  • Posts: 4219
Well, I would differentiate between things that play, i.e. humans an the AI, and things that get played, that is personages or roles or however you want to call them.

But I've only ever written one game in all my life (and that was in Turbo Basic and it was never finished) so I may be completely wrong. :)
Turbo Pascal 3 CP/M - Amstrad PCW 8256 (512 KB !!!) :P
Lazarus/FPC 2.0.8/3.0.4 & 2.0.12/3.2.0 - 32/64 bits on:
(K|L|X)Ubuntu 12..18, Windows XP, 7, 10 and various DOSes.

Handoko

  • Hero Member
  • *****
  • Posts: 5154
  • My goal: build my own game engine using Lazarus
I used Turbo Basic too. And I loved it so much. As its name suggests, it really run much faster than the other BASICs of that time.

Turbo Basic later renamed to PowerBASIC. Sad news, the creator of Turbo/Power BASIC Robert Bob Zale has passed away some years ago.

 

TinyPortal © 2005-2018