The Alchemist
Game project in C++ / DirectX11 using the Overlord Engine. Top-down single player game made in four weeks.




The player can shoot arrows and use abilities such as healing and fear. A quiver class holds abilities and their cooldowns.
void Quiver::Initialize(const GameContext& gameContext)
{
for (UINT i = 0; i < m_AmountOfArrows; i++)
{
auto arrow = new Arrow();
m_pArrowsVec.push_back(arrow);
AddChild(arrow);
}
m_pHealingPotion = new HealingPotion;
AddChild(m_pHealingPotion);
m_PanicBolt = new PanicBolt;
AddChild(m_PanicBolt);
m_ClarityPotion = new ClarityPotion;
AddChild(m_ClarityPotion);
m_ToxicPool = new ToxicPool;
AddChild(m_ToxicPool);
}
void Quiver::Update(const GameContext & gc)
{
for (auto & skill : m_SkillsData)
{
auto & canCast = std::get<0>(skill);
if (!canCast)
{
auto & totalCdTime = std::get<1>(skill);
auto & currentCd = std::get<2>(skill);
if (currentCd > totalCdTime)
{
currentCd = 0;
canCast = true;
}
else
{
currentCd += gc.pGameTime->GetElapsed();
}
}
}
}
void Quiver::FireArrow(const DirectX::XMFLOAT3 & pos, float yrot)
{
//std::cout << "Fire arrow" << std::endl;
auto & canCast = std::get<0>(m_SkillsData[0]);
if (!canCast)
{
return;
}
std::list<Arrow *>::iterator arrowIt;
for (arrowIt = m_pArrowsVec.begin(); arrowIt != m_pArrowsVec.end() ; ++arrowIt)
{
auto & arrow = *arrowIt;
if (!arrow->m_IsFlying )// && ! arrow->m_IsStuck)
{
arrow->Launch(pos, yrot);
m_pArrowsVec.emplace_back(arrow);
m_pArrowsVec.erase(arrowIt);
canCast = false;
return;
}
}
}
class Quiver : public GameObject
{
public:
//A quiver is a container for holding different abilities (arrows & vials)
Quiver();
virtual ~Quiver() = default;
Quiver(const Quiver& other) = delete;
Quiver(Quiver&& other) noexcept = delete;
Quiver& operator=(const Quiver& other) = delete;
Quiver& operator=(Quiver&& other) noexcept = delete;
void Initialize(const GameContext& gameContext) override;
void PostInitialize(const GameContext& gameContext) override;
void Update(const GameContext& gameContext) override;
void PostDraw(const GameContext & gameContext) override;
void FireArrow(const DirectX::XMFLOAT3 & pos, float yrot);
void ThrowHealingPot(const DirectX::XMFLOAT3 & charPos, const DirectX::XMFLOAT3 & targetPos);
void Shield();
void FirePanicBolt(const DirectX::XMFLOAT3 & pos, float yrot);
void ThrowClarityPot(const DirectX::XMFLOAT3 & charPos, const DirectX::XMFLOAT3 & targetPos);
void ThrowToxicPool(const DirectX::XMFLOAT3 & charPos, const DirectX::XMFLOAT3 & targetPos);
UINT m_AmountOfArrows = 60;
std::list<Arrow *> m_pArrowsVec;
HealingPotion * m_pHealingPotion;
Character * m_pPlayer;
PanicBolt * m_PanicBolt;
ClarityPotion * m_ClarityPotion;
ToxicPool * m_ToxicPool;
//CoolDown list
// 0 : Can Cast // 1 : Total Cooldown Time // 2: Current CoolDown time // 3: Energy Required // 4: id // 5: Name
std::vector<std::tuple<bool, float, float, float, int, std::string>> m_SkillsData =
{
{true, 0.7f, 0.f, 0.f , 0, "ToxicBolt"}, //0
{true, 3.f, 0.f, 0.f , 1, "HealingPotion"}, //1
{true, 4.f, 0.f, 0.f , 2, "ClarityPotion"}, //2
{true, 10.f, 0.f, 0.f , 3,"PanicBolt"}, //3
{true, 30.f, 0.f, 100.f,4, "ToxicGoo"}, //4
{true, 1.f, 0.f, 25.f, 5,"InjectionEx"}, //5
{true, 1.f, 0.f, 25.f, 6,"PetrifyEx"}, //6
{true, 20.f, 0.f, 0.f, 7,"Shield"} //7
};
protected:
QuadDrawer * m_pDrawer, *m_pDrawer2, *m_pDrawer3, *m_pDrawer4, *m_pDrawer5, *m_pDrawer6;
IconMaterial * m_pIconMat;
};
The enemy has a state machine with running, attacking, chasing, and idle states. AiState is the virtual base. Once the player comes in range, the enemy switches to AttackAiState.
std::pair<bool, AiState*> AttackAiState::Update(const GameContext& gc)
{
auto enemyT = m_Owner.GetTransform();
auto targetT = m_pTarget->GetTransform();
auto pos = enemyT->GetPosition();
auto targetPos = targetT->GetPosition();
auto dt = gc.pGameTime->GetElapsed();
auto xvec = (targetPos.x - pos.x);
auto yvec = (targetPos.z - pos.z);
auto dist = sqrt(xvec * xvec + yvec * yvec);
xvec /= dist;
yvec /= dist;
if (m_Owner.m_IsFeared)
{
xvec = -xvec;
yvec = -yvec;
}
auto vecToTarget = DirectX::XMFLOAT3(xvec * dt * 15 * m_Owner.m_Move, -1, yvec * dt * 15 * m_Owner.m_Move);
if (m_Owner.m_IsSlowed) {
vecToTarget.x *= 0.5;
vecToTarget.y *= 0.5;
}
auto angle = acos(-yvec);
if(!m_Owner.m_IsAttacking)
m_Owner.m_pController->Move(vecToTarget);
if (xvec > 0)
m_Owner.m_pCharacterModel->GetTransform()->Rotate(DirectX::XMFLOAT3(0.0f, -angle, 0.0f), false);
else
m_Owner.m_pCharacterModel->GetTransform()->Rotate(DirectX::XMFLOAT3(0.0f, angle, 0.0f), false);
if (dist < 8)
{
m_Owner.m_IsAttacking = true;
m_Owner.m_pCharacterModel->GetAnimator()->SetAnimation(3);
}
return { false, new IdleAiState(m_Owner) };
}
The health shader uses a procedural animated material created by blending multiple texture samples displaced over time in different directions.
// Vertex Shader
VS_OUTPUT VS(VS_INPUT input){
VS_OUTPUT output;
output.pos = float4(input.pos, 1.0f);
output.texCoord = input.texCoord;
return output;
}
// Pixel Shader
PS_OUTPUT PS(VS_OUTPUT input) : SV_TARGET{
PS_OUTPUT output = (PS_OUTPUT)0;
float hpscale = 0.01 * (100-gHealth) * 0.17;
if (input.texCoord.y < 0.83 + hpscale)
{
return output;
}
output.color = 0;
float res = 1 / 1024.0;
float2 tex = input.texCoord;
tex.x = tex.x * 5.51;//
tex.y = tex.y * 5.1- 0.017*gTime;
float4 sam = gSourceMap.Sample(samLinear, tex);
tex.y = tex.y * 0.61 - 0.014*gTime;
tex.x += 0.5;
float4 sam2 = gSourceMap.Sample(samLinear, tex);
tex.x = tex.x * 2.3 - 0.025 * gTime;
tex.y = tex.y * 1.3 - 0.012 * gTime;
float4 sam3 = gSourceMap.Sample(samLinear, tex);
tex.x = tex.x * 1.3 + 0.1542 * gTime;
tex.y = tex.y * 2 - 0.04 * gTime;
float4 sam4 = gSourceMap.Sample(samLinear, tex);
float4 alpha = gAlphaMap.Sample(samLinear, input.texCoord);
if (sam.a < 0.05)
{
discard;
}
output.color = lerp(lerp(sam, sam2 +sam3 * 0.5, 0.5), lerp(sam3, sam4, 0.5), 0.5);
output.color.a = alpha.r + alpha.b + alpha.g ;
output.color.rgb = changeSaturation(output.color.rgb, 7);
output.color = pow(output.color, 0.75);
output.color.a = 0.85 * 0.33 * output.color.a * (output.color.r+.3);
return output;
}
float3 changeSaturation(float3 RGB, float change) {
// public-domain function by Darel Rex Finley
float G;
float B;
float R;
R = RGB.r;
G = RGB.g;
B = RGB.b;
float Pr = 0.299;
float Pg = 0.587;
float Pb = 0.114;
float P = sqrt(
(R)*(R)*Pr +
(G)*(G)*Pg +
(B)*(B)*Pb);
R = P + ((R)-P)*change;
G = P + ((G)-P)*change;
B = P + ((B)-P)*change;
return float3(R, G, B);
}
The toxic pool attack combines texture warping, blending, and the geometry shader.
// GEOMETRY SHADER
void ToxicGenerator(triangle VS_DATA vertices[3], inout TriangleStream<GS_DATA> triStream)
{
CreateVertex(triStream, vertices[0].Position, vertices[0].Normal, vertices[0].TexCoord * .1 , 1);
CreateVertex(triStream, vertices[1].Position, vertices[1].Normal, vertices[1].TexCoord * .1 , 1);
CreateVertex(triStream, vertices[2].Position, vertices[2].Normal, vertices[2].TexCoord * .1 , 1);
triStream.RestartStrip();
float pos1 = vertices[0].Position;
float pos2 = vertices[1].Position;
float pos3 = vertices[2].Position;
for (int i = 1; i < 15; i++)
{
float f = 1 / i;
int c = i * (3 + sin(pow(m_Time,f)));
float angle2 = (0.05*(m_Time+i)) * .37;
angle2 = 0;
float2x2 uvRotation2 = { cos(angle2), -sin(angle2), sin(angle2), cos(angle2) };
CreateVertex(triStream, pos1, vertices[0].Normal, (mul(uvRotation2,(vertices[0].TexCoord -0.5))+0.5) * (0.42 - (0.03 * i)), 0.002);
CreateVertex(triStream, pos2, vertices[1].Normal, (mul(uvRotation2,(vertices[1].TexCoord -0.5))+0.5) * (0.42 - (0.03 * i)), 0.002);
CreateVertex(triStream, pos3, vertices[2].Normal, (mul(uvRotation2,(vertices[2].TexCoord -0.5))+0.5) * (0.42 - (0.03 * i)), 0.002);
triStream.RestartStrip();
}
}
// PIXEL SHADER *
float4 MainPS(GS_DATA input) : SV_TARGET
{
float x = input.TexCoord.x;
float y = input.TexCoord.y;
float distance = sqrt((x * x) + (y * y));
if (distance > 0.25)
{
discard;
}
float angle = + m_Time;
float2x2 uvRotation = { cos(angle), -sin(angle), sin(angle), cos(angle) };
float2 uv = mul(input.TexCoord, uvRotation);
float4 r = m_Texture1.Sample(samLinearb, uv + 0.5);
float4 r12 = m_Texture2.Sample(samLinear, input.TexCoord);
float angle2 = -m_Time * .37 + 0.8;
float2x2 uvRotation2 = { cos(angle2), -sin(angle2), sin(angle2), cos(angle2) };
float2 uv2 = mul(input.TexCoord, uvRotation2);
float4 r2 = m_Texture1.Sample(samLinearb, uv2 + 0.5);
float4 r22 = m_Texture2.Sample(samLinear, input.TexCoord + m_Time * 0.1);
float angle3 = m_Time * .17 + 8.8;
float2x2 uvRotation3 = { cos(angle3), -sin(angle3), sin(angle3), cos(angle3) };
float2 uv3 = mul(input.TexCoord, uvRotation3);
float4 r3 = m_Texture1.Sample(samLinearb, uv3 + 0.5);
float4 r32 = m_Texture2.Sample(samLinear, input.TexCoord - cos(m_Time * 0.1));
float angle4 = 0.3* cos(m_Time) + 0.7 * sin(m_Time+.7) * .27 + 18.8;
float2x2 uvRotation4 = { cos(angle4), -sin(angle4), sin(angle4), cos(angle4) };
float2 uv4 = mul(input.TexCoord, uvRotation4);
float4 r4 = m_Texture1.Sample(samLinearb, uv4 + 0.5);
float4 r42 = m_Texture2.Sample(samLinear, input.TexCoord + sin(m_Time *0.73));
r *= 0.9;
r2 *= 0.9;
r3 *= 0.9;
r4 *= 0.9;
float4 ret = r * sin(r2) * r3 * r4;
float4 ret2 = r12 * r22 * r32 * r42;
float avg = ret.r + ret.g + ret.b;
ret.g *= 2.8;
if (ret.r + ret.g + ret.b < 0.01)
ret.a -= 1 - (50 *avg);
ret.g += 6*avg;
ret.b -= 2 * avg;
ret.r -= .2 * avg;
if (distance > 0.20)
{
ret *= 0.07;
}
if (distance > 0.15)
{
ret *= 0.24;
}
return pow(ret , 1);
}