Official documentation of Casual Game Engine
Let us get more into coding by examining an example entity included within the builder template: The headcrab.
The headcrab is a fanmade re-creation of the headcrab of the popular Half-Life series. In our example it shall walk around and attack the player when nearby. Otherwise it walks around to different directions.
So, let’s assume we have an empty class implementing the IScriptedEntity interface.
class CHeadcrabEntity : IScriptedEntity {
//Interface implementation...
}
One of the first things we would like to to is to load the sprite file that represents the headcrab in the scene. We can load the sprite using R_LoadSprite(). This shall be done in the OnSpawn() method. We do also want to create a bounding box associated with a model which is used for collisions.
Vector m_vecPos;
Vector m_vecSize;
float m_fRotation;
SpriteHandle m_hSprite;
Model m_oModel;
CHeadcrabEntity()
{
this.m_vecSize = Vector(32, 32);
this.m_fRotation = 0.0f;
}
void OnSpawn(const Vector& in vec)
{
this.m_vecPos = vec;
this.m_hSprite = R_LoadSprite(GetPackagePath() + "gfx\\headcrab.png", 1, 32, 32, 1, true);
BoundingBox bbox;
bbox.Alloc();
bbox.AddBBoxItem(Vector(0, 0), this.m_vecSize);
this.m_oModel.Alloc();
this.m_oModel.Initialize2(bbox, this.m_hSprite);
}
R_LoadSprite accepts the following parameters:
R_LoadSprite returns a valid sprite handle (SpriteHandle) on success. Be sure to store the handle into a class member property.
We do also create a bounding box that holds the cubes of the model which is used for collision detection. You can add multiple cubes to a bounding box which will all be taken into account for collision detection. The bounding box will be attached to a model which holds the bounding box and the main sprite.
AddBBoxItem receives two arguments:
The model will then be initialized with the bounding box and the main sprite handle. Note that you have allocate bounding boxes and models before using them.
After that we now want to draw the entity sprite at the given position. We will need to calculate the screen position according to the current world position
void OnDrawOnTop()
{
if (!R_ShouldDraw(this.m_vecPos, this.m_vecSize))
return;
Vector vOut;
R_GetDrawingPosition(this.m_vecPos, this.m_vecSize, vOut);
R_DrawSprite(this.m_hSprite, vOut, 0, this.m_fRotation, Vector(-1, -1), 0.0, 0.0, false, Color(0, 0, 0, 0));
}
At first we determine if the entity is in screen range. If not, we abort the drawing process. Otherwise we let the engine calculate and provide us with the screen drawing position of the entity. After that we draw the entity. R_DrawSprite receives the following arguments:
Note you can also use the OnDraw() method, but it should preferably be used for things such as floors or decals, basically everything that represents something on the ground.
So, now it doesn’t do much. We want it to walk around. So we need a timer and calculate the movement when the timer is elapsed.
//Member declaration
Timer m_oMovement;
//In OnSpawn()
this.m_oMovement.SetDelay(100);
this.m_oMovement.Reset();
this.m_oMovement.SetActive(true);
//In OnProcess()
this.m_oMovement.Update();
if (this.m_oMovement.IsElapsed()) {
this.m_oMovement.Reset();
Ent_Move(this, this.m_fSpeed, MOVE_FORWARD);
}
The entity will move whenever the timer is elasped. Since we want the entity to continuously walk, we will reset the timer whenever it is elapsed. We use the Ent_Move() engine function in order to move our entity. The engine will actually calculate our new world position according to our rotation, speed and direction. Ent_Move accepts the following arguments:
If the entity would hit a wall then it will not move forward, but remain at the current position In order to apply the new position and provide the engine with our data, we need to implement four methods:
//Called for recieving the current position. This is useful if the entity shall move.
Vector& GetPosition()
{
return this.m_vecPos;
}
//Set new position
void SetPosition(const Vector &in vecPos)
{
this.m_vecPos = vecPos;
}
//Return the rotation.
float GetRotation()
{
return this.m_fRotation;
}
//Set new rotation
void SetRotation(float fRot)
{
this.m_fRotation = fRot;
}
Now our entity will move forward. Now we do also want to change its direction randomly and also play a walking sound:
Timer m_oDirChange;
Timer m_oWalkSound;
SoundHandle m_hWalkSound;
//In OnSpawn():
this.m_oDirChange.SetDelay(5000);
this.m_oDirChange.Reset();
this.m_oDirChange.SetActive(true);
this.m_oWalkSound.SetDelay(1000 + Util_Random(1, 2000));
this.m_oWalkSound.Reset();
this.m_oWalkSound.SetActive(true);
this.m_hWalkSound = S_QuerySound(GetPackagePath() + "sound\\hc_walk.wav");
//In OnProcess():
this.m_oDirChange.Update();
if (this.m_oDirChange.IsElapsed()) {
this.m_oDirChange.Reset();
if (!this.m_bGotEnemy)
this.m_fRotation = float(Util_Random(1, 360));
}
this.m_oWalkSound.Update();
if (this.m_oWalkSound.IsElapsed()) {
this.m_oWalkSound.Reset();
S_PlaySound(this.m_hWalkSound, S_GetCurrentVolume());
}
We will set the direction change timer to a random delay value. You can generate random numbers bewtween a range via Util_Random(). Sounds can be loaded via S_QuerySound(). This will load a sound into memory if it has not yet been loaded. You provide the full path to the file as the only argument. A sound can then be played via S_PlaySound(). It accepts the handle to the sound as first argument and the volume (0 - 10) as last argument. You can query the current game volume via S_GetCurrentVolume().
Next thing is that we want to attack the player if the headcrab is nearby. Therefore we check repeatedly if the player is nearby and then run towards the player. If the headcrab is close enough we will damage the player entity every elpased timespan. For that we need the following two methods:
void LookAt(const Vector &in vPos)
{
//Look at position
float flAngle = atan2(float(vPos[1] - this.m_vecPos[1]), float(vPos[0] - this.m_vecPos[0]));
this.m_fRotation = flAngle + 6.30 / 4;
}
void CheckForEnemiesInRange()
{
//Check for enemies in close range and act accordingly
this.m_bGotEnemy = false;
IScriptedEntity@ pEntity = null;
for (size_t i = 0; i < Ent_GetEntityCount(); i++) {
@pEntity = @Ent_GetEntityHandle(i);
if ((@pEntity != null) && (pEntity.GetName() == "player")) {
if (this.m_vecPos.Distance(pEntity.GetPosition()) <= C_HEADCRAB_REACT_RANGE) {
this.m_bGotEnemy = true;
break;
}
}
}
if (this.m_bGotEnemy) {
if (this.m_fSpeed == C_HEADCRAB_DEFAULT_SPEED)
this.m_fSpeed *= 2;
if (pEntity.GetName().length() > 0) {
this.LookAt(pEntity.GetPosition());
}
if (this.m_vecPos.Distance(pEntity.GetPosition()) <= C_HEADGRAB_ATTACK_RANGE) {
this.m_oAttack.Update();
if (this.m_oAttack.IsElapsed()) {
pEntity.OnDamage(C_HEADGRAB_DAMAGE_VALUE);
S_PlaySound(this.m_hAttackSound, S_GetCurrentVolume());
this.m_oAttack.Reset();
}
}
} else {
if (this.m_fSpeed != C_HEADCRAB_DEFAULT_SPEED)
this.m_fSpeed = C_HEADCRAB_DEFAULT_SPEED;
}
}
The first method sets our rotation accordingly to look at the target entity by receiving its position. The second method does the following:
In order to let the headcrab take damage and be defeated we will now also implement this behaviour:
//Amon other member variables:
uint32 m_uiHealth;
//In OnProcess():
if (this.m_tmrFlicker.IsActive()) {
this.m_tmrFlicker.Update();
if (this.m_tmrFlicker.IsElapsed()) {
this.m_tmrFlicker.Reset();
this.m_uiFlickerCount++;
if (this.m_uiFlickerCount >= 6) {
this.m_tmrFlicker.SetActive(false);
this.m_uiFlickerCount = 0;
}
}
}
//In OnDrawOnTop():
Color sDrawingColor = (this.m_tmrFlicker.IsActive()) ? Color(255, 0, 0, 150) : Color(0, 0, 0, 0);
bool bCustomColor = (this.m_tmrFlicker.IsActive()) && (this.m_uiFlickerCount % 2 == 0);
R_DrawSprite(this.m_hSprite, vOut, 0, this.m_fRotation, Vector(-1, -1), 0.0, 0.0, bCustomColor, sDrawingColor);
//Called when entity gets damaged
void OnDamage(uint32 damageValue)
{
if (this.m_uiHealth < damageValue) {
this.m_uiHealth = 0;
} else {
this.m_uiHealth -= damageValue;
}
this.m_tmrFlicker.Reset();
this.m_tmrFlicker.SetActive(true);
this.m_uiFlickerCount = 0;
}
bool NeedsRemoval()
{
return this.m_uiHealth == 0;
}
You should now be able to undertand what the code is doing.
Also do not forget to provide your entity instance with a unit name and also indicate to the engine that this entity is collidable:
//Indicate whether this entity is collidable
bool IsCollidable()
{
return true;
}
//Return a name string here, e.g. the class name or instance name.
string GetName()
{
return "headcrab";
}