I have a nice update for everyone.
I was able to convert everything over to the class-based system nicely. Here is a newer draft-draft pre-alpha weapon.lua script with all the functionality I want currently except Secondary Fire mode, Melee attack, and the handling of automatic fire:
Here is the gold_xix.lua first, so you can see how relatively easy it is (this is all you have to do for your gun setup-wise):
dofile("scripts/classes/weapon.lua")
GoldXIX = { }
GoldXIX_WeaponTable = {
ViewModel = "abstract::HUD.gmf",
Offset = Vec3(0.0,-0.005,0.05),
Scale = Vec3(0.04,0.04,0.04)
}
GoldXIX = CreateWeapon(GoldXIX_WeaponTable)
-- Properties
GoldXIX.Primary.Delay = 400
-- Animations
GoldXIX:AddAnimation("WEAPON_IDLE", 277.0, 302.0, 1.0)
GoldXIX:AddAnimation("WEAPON_ZOOM_IDLE", 212.0, 238.0, 1.0)
GoldXIX:AddAnimation("WEAPON_EMPTY_IDLE", 779.0, 804.0, 1.0)
GoldXIX:AddAnimation("WEAPON_MOVE", 304.0, 326.0, 2.0)
GoldXIX:AddAnimation("WEAPON_ZOOM_MOVE", 239.0, 261.0, 2.0)
GoldXIX:AddAnimation("WEAPON_EMPTY_MOVE", 805.0, 829.0, 2.0)
GoldXIX:AddAnimation("WEAPON_TO_ZOOM", 180.0, 187.0, 2.0, true)
GoldXIX:AddAnimation("WEAPON_FROM_ZOOM", 188.0, 194.0, 2.0, true)
GoldXIX:AddAnimation("WEAPON_FIRE", 62.0, 74.0, 3.0, true)
GoldXIX:AddAnimation("WEAPON_ZOOM_FIRE", 262.0, 274.0, 3.0, true)
GoldXIX:AddAnimation("WEAPON_FULL_RELOAD", 82.0, 150.0, 3.0, true)
GoldXIX:AddAnimation("WEAPON_TACTICAL_RELOAD", 640.0, 690.0, 3.0, true)
GoldXIX:AddAnimation("WEAPON_MINIMAL_RELOAD", 581.0, 639.0, 3.0, true)
GoldXIX:AddAnimation("WEAPON_TAKE_OUT", 50.0, 58.0, 1.0, true)
GoldXIX:AddAnimation("WEAPON_PUT_AWAY", 152.0, 159.0, 1.0, true)
-- Initial Animation
GoldXIX:SetState(WEAPON_IDLE)
-- Sounds
GoldXIX:AddSound("Fire", "abstract::fire.ogg", 65)
GoldXIX:AddSound("ZoomFire", "abstract::fire.ogg", 265)
GoldXIX:AddSound("EmptyFire", "abstract::fire.ogg", 697)
GoldXIX:AddSound("WEAPON_DRYFIRE", "abstract::dryfire.ogg", 0)
GoldXIX:AddSound("FullReload", "abstract::reload.ogg", 85)
GoldXIX:AddSound("TacticalReload", "abstract::reload.ogg", 644)
GoldXIX:AddSound("MinimalReload", "abstract::reload.ogg", 585)
GoldXIX:AddSound("FullCock1", "abstract::cock2.ogg", 135)
GoldXIX:AddSound("FullCock2", "abstract::cock2.ogg", 143)
GoldXIX:AddSound("MinimalCock", "abstract::cock2.ogg", 629)
And here are the snippets for all that you have to call in your render loop (this will be abstracted into its own little thing and use a simple WeaponSystem:Update() or something to check all weapons in the table):
GoldXIX:Update()
if (KeyHit(KEY_W)-KeyHit(KEY_S) ~= 0 or KeyHit(KEY_A)-KeyHit(KEY_D) ~= 0 and GoldXIX:GetState() == WEAPON_IDLE) then
GoldXIX.Moving = true
end
if (KeyDown(KEY_W)-KeyDown(KEY_S) == 0 and KeyDown(KEY_A)-KeyDown(KEY_D) == 0 and GoldXIX:GetState() == WEAPON_MOVE) then
GoldXIX.Moving = false
end
if (MouseHit(2) == 1) then
GoldXIX:ToggleIronsights()
end
if (KeyHit(KEY_1) == 1) then
GoldXIX:Toggle()
end
if KeyHit(KEY_R)==1 then
GoldXIX:Reload()
end
if MouseHit(1)==1 then
GoldXIX:PrimaryFire()
end
The library is pretty robust for the 546 lines that comprise the weapon.lua, and 231 lines in bullet.lua. They will both receive their upgrades as I start working my way through the rest of the feature tree, starting with the core stuff, and working towards eye candy and fun stuff (like akimbo and laz0rs ).
Note that the commandset called in the game-loop is really that easy. The weapon's finite state logic and update loops will handle itself efficiently and properly, and has built in checks for commonly missed things like firing while reloading, etc. though bugs may exist and it will be improved. With permission I will either put a section with an API for this on the Wiki, or in its own thread.
weapon.lua:
dofile("scripts/utilities.lua")
-- Defaults
-- States
WEAPON_IDLE = 0
WEAPON_TAKE_OUT = 1
WEAPON_PUT_AWAY = 2
WEAPON_AWAY = 3
WEAPON_FIRE = 4
WEAPON_RELOAD = 5
WEAPON_ZOOM_IDLE = 6
WEAPON_ZOOM_FIRE = 7
WEAPON_TO_ZOOM = 8
WEAPON_TO_NORMAL = 9
WEAPON_MOVING = 10
WEAPON_ZOOM_MOVING = 11
-- Globals
WEAPON_SCALE = 0.6 -- Global scale shared by all weapons. Gun specific scaling is multiplied by this factor. It allows guns of different scales to appear similarly sized.
-- Variables
firstweapon=nil
lastweapon=nil
-- Weapons Table
weapons={}
function CreateWeapon( weapon_table )
local weapon = {}
-- Sub Tables
weapon.Animations = { } -- Table containing all animations
weapon.Sounds = { } -- Table containing all sounds
weapon.Effects = { } -- Table containing all effects (emitter, muzzle flash, brass, etc.)
weapon.Attachments = { } -- Table containing attachments (ironsights, scopes, etc.)
-- Properties
weapon.AnimationBlending = 0.5
weapon.SwayScale = 0 -- X Axis Movement
weapon.BobScale = 0 -- Y Axis Movement
weapon.Zoomed = false
weapon.Moving = false
weapon.AutoReload = false -- Gun will automatically go to reload state if you try to fire with an empty clip
weapon.Visible = true -- Start with the gun visible
weapon.Selected = true -- Start with the gun selected
-- Sound
weapon.Volume = 1.0
weapon.Pitch = 1.0
-- Animation
weapon.AnimationStartTime = AppTime()
weapon.CurrentAnimation = ""
weapon.CurrentAnimationFrame = 0
weapon.Animating = false
-- Primary
weapon.Primary = { }
weapon.Primary.ClipSize = 12 -- Number of rounds in the clip
weapon.Primary.Clip = 12 -- Current number of rounds in the clip
weapon.Primary.Ammo = 240 -- Current total ammo (not including current clip)
weapon.Primary.Damage = 0 -- Damage done per round
weapon.Primary.ShotCount = 0 -- Bullets ejected per round
weapon.Primary.Cone = 0 -- Spread cone (X,Y)
weapon.Primary.Delay = 0 -- Time between current shot and next shot
weapon.Primary.NextFire = AppTime() -- Time next shot can be fired
-- Secondary
weapon.Secondary = { }
weapon.Secondary.ClipSize = 0
weapon.Secondary.Clip = 0
weapon.Secondary.Ammo = 0
weapon.Secondary.Damage = 0
weapon.Secondary.ShotCount = 0
weapon.Secondary.Cone = 0
weapon.Secondary.Delay = 0
weapon.Secondary.NextFire = AppTime()
-- Finite State Logic
weapon.State = 0 -- Weapons's current logic state
-- World/View Models
weapon.WorldModel = nil -- World View Model
weapon.ViewModel = LoadMesh(weapon_table.ViewModel) -- First Person View Model
weapon.ViewModel:SetParent(fw.main.camera,0)
weapon.GunOffset = weapon_table.Offset
weapon.GunScale = weapon_table.Scale
weapon.ViewModel:SetPosition(Vec3(weapon.GunOffset.x*WEAPON_SCALE,weapon.GunOffset.y*WEAPON_SCALE,weapon.GunOffset.z*WEAPON_SCALE),0)
weapon.ViewModel:SetScale(Vec3(weapon.GunScale.x*WEAPON_SCALE,weapon.GunScale.y*WEAPON_SCALE,weapon.GunScale.z*WEAPON_SCALE))
weapon.ViewModel:SetShadowMode(0,1)
AppLog("We loaded and positioned the view model...")
-- AABB Culling Fix...
weapon.ViewModel.localAABB.x0=-3
weapon.ViewModel.localAABB.x1=3
weapon.ViewModel.localAABB.y0=-3
weapon.ViewModel.localAABB.y1=3
weapon.ViewModel.localAABB.z0=-3
weapon.ViewModel.localAABB.z1=3
weapon.ViewModel:UpdateAABB()
-- Positioning / Offsets
weapon.Offset = weapon.ViewModel.position:Copy()
local gundisplayposition = weapon.ViewModel:GetPosition()
local positionentity = FindChild(weapon.ViewModel,"FIRESPOT") or weapon.ViewModel -- Entity used for firespot location
local displayposition = EntityPosition(positionentity)
weapon.FireSpot = positionentity
-- Muzzleflash
weapon.Effects.MuzzleFlash = CreatePointLight(3)
weapon.Effects.MuzzleFlash:SetParent( weapon.ViewModel )
local muzzleflash_color = weapon_table.muzzleflash_color or Vec4(1,0.6,0.0,1.0)
weapon.Effects.MuzzleFlash:SetColor(muzzleflash_color)
weapon.Effects.MuzzleFlash:SetPosition( displayposition )
weapon.Effects.MuzzleFlash:SetShadowMode(0)
weapon.Effects.MuzzleFlash:Hide()
function weapon:Update()
if (not self.Selected and not self.Visible) then return end
-- Idle State
if (self:IsIdle()) then
if (self:IsZoomed() and not self:IsPrimaryClipEmpty()) then
if (self:IsMoving()) then
self:PlayAnimation("WEAPON_ZOOM_MOVE")
else
self:PlayAnimation("WEAPON_ZOOM_IDLE")
end
else
if (self:IsMoving()) then
if (self:IsPrimaryClipEmpty()) then
self:PlayAnimation("WEAPON_EMPTY_MOVE")
else
self:PlayAnimation("WEAPON_MOVE")
end
else
if (self:IsPrimaryClipEmpty()) then
self:PlayAnimation("WEAPON_EMPTY_IDLE")
else
self:PlayAnimation("WEAPON_IDLE")
end
end
end
-- Fire State
elseif (self:IsFiring()) then
-- Zoom Fire State
if (self:IsZoomed()) then
self:SetAnimating(true)
self:PlayAnimation("WEAPON_ZOOM_FIRE")
-- Normal Fire State
else
self:SetAnimating(true)
self:PlayAnimation("WEAPON_FIRE")
end
if (not self:IsAnimating()) then
self:SetState(WEAPON_IDLE)
end
-- Reloading State
elseif (self:IsReloading()) then
if (self:GetPrimaryClip() == 0) then
self:PlayAnimation("WEAPON_FULL_RELOAD")
else
if (self:GetPrimaryClip() >= self:GetPrimaryClipSize()/2) then
self:PlayAnimation("WEAPON_TACTICAL_RELOAD")
else
self:PlayAnimation("WEAPON_MINIMAL_RELOAD")
end
end
if (not self:IsAnimating()) then
self:SetState(WEAPON_IDLE)
self:DoReload()
end
-- Taking Out State
elseif (self:IsTakingOut()) then
self:PlayAnimation("WEAPON_TAKE_OUT")
if (not self:IsAnimating()) then
self:SetState(WEAPON_IDLE)
end
-- Putting Away State
elseif (self:IsPuttingAway()) then
self:PlayAnimation("WEAPON_PUT_AWAY")
if (not self:IsAnimating()) then
self:SetState(WEAPON_AWAY)
self.Selected = false
self.Visible = false
self.ViewModel:Hide()
end
-- No State? (Idle)
else
self:PlayAnimation("WEAPON_IDLE")
end
self.ViewModel:SetPosition(self.Offset:Copy())
self:UpdateSounds()
end
function weapon:Think()
-- This can be used for AI, etc. Implement it if you want
end
-- Animation
function weapon:AddAnimation(name,startframe,endframe,speedmodifier,oneshot)
self.Animations[name] = {Name = name,
StartFrame = startframe,
EndFrame = endframe,
Length = (endframe-startframe),
Modifier = speedmodifier,
OneShot = oneshot or false,
Running = false}
end
function weapon:LoopAnimation(animation)
local time = ((AppTime() - self.AnimationStartTime) / 100.0)
local currentanim = self.Animations[animation]
self.CurrentAnimationFrame = ((time * currentanim.Modifier * HOST_TIMESCALE) % currentanim.Length) + currentanim.StartFrame
if (self.CurrentAnimationFrame < currentanim.StartFrame) then self.CurrentAnimationFrame = currentanim.StartFrame end
if (self.CurrentAnimationFrame > currentanim.EndFrame) then self.CurrentAnimationFrame = currentanim.EndFrame end
self.ViewModel:Animate(self.CurrentAnimationFrame, self.AnimationBlending, 0, 1)
end
function weapon:SetAnimation(animation)
if (self.CurrentAnimation ~= "") then
self.Animations[self.CurrentAnimation].Running = false
end
self.CurrentAnimation = animation
self.Animations[animation].Running = true
self.Animating = true
self.AnimationStartTime = AppTime()
end
function weapon:PlayAnimation(animation)
if (not self.Animating) then return end
if (animation == nil or animation == "") then return end
local time = (AppTime() - self.AnimationStartTime) / 100.0
local currentanim = self.Animations[animation]
currentanim.Running = true
if (not self.Animating) then
currentanim.Running = false
end
self.CurrentAnimationFrame = (time * currentanim.Modifier * HOST_TIMESCALE) % currentanim.Length + currentanim.StartFrame
if (self.CurrentAnimationFrame < currentanim.StartFrame) then self.CurrentAnimationFrame = currentanim.StartFrame end
if (self.CurrentAnimationFrame > currentanim.EndFrame) then self.CurrentAnimationFrame = currentanim.EndFrame end
self.ViewModel:Animate(self.CurrentAnimationFrame, self.AnimationBlending, 0, 1)
if ((currentanim.OneShot and self.CurrentAnimationFrame >= currentanim.EndFrame-1.0) or currentanim.Running == false) then
self.Animating = false
currentanim.Running = false
end
end
-- Sound
function weapon:AddSound(name,soundfile,frame)
local sound = LoadSound(soundfile)
local sound_table = {Name = name,
Frame = frame,
Sound = sound,
Source = CreateSource(sound)}
local Weapon = self
function sound_table:Update()
self.Source:SetVolume(Weapon.Volume)
self.Source:SetPitch(Weapon.Pitch*HOST_TIMESCALE)
end
function sound_table:Play()
self:Update() -- Update volume and pitch
self.Source:Play() -- Play source
end
self.Sounds[name] = sound_table
end
function weapon:UpdateSounds()
for k,v in pairs(self.Sounds) do
if (v.Frame == math.floor(self.CurrentAnimationFrame)) then
v:Play()
end
end
end
function weapon:PlaySound(name)
if (self.Sounds[name] ~= nil) then
self.Sounds[name]:Play()
end
end
-- States
function weapon:GetState()
return self.State
end
function weapon:SetState(state)
self.State = state
self.AnimationStartTime = AppTime()
self.Animating = true
end
function weapon:IsIdle()
return self.State == WEAPON_IDLE or self.State == WEAPON_ZOOM_IDLE
end
function weapon:IsFiring()
return self.State == WEAPON_FIRE
end
function weapon:IsReloading()
return self.State == WEAPON_RELOAD
end
function weapon:IsZoomed()
return self.State == WEAPON_ZOOM_IDLE or self.State == WEAPON_ZOOM_FIRE or self.Zoomed
end
function weapon:IsEmpty()
return self:IsPrimaryClipEmpty()
end
function weapon:IsAnimating()
return self.Animating
end
function weapon:IsMoving()
return ((KeyDown(KEY_W) - KeyDown(KEY_S)) ~= 0) or ((KeyDown(KEY_A) - KeyDown(KEY_D)) ~= 0)
end
function weapon:IsTakingOut()
return self.State == WEAPON_TAKE_OUT
end
function weapon:IsPuttingAway()
return self.State == WEAPON_PUT_AWAY
end
function weapon:IsAway()
return self.State == WEAPON_AWAY
end
function weapon:IsSelected()
return self.Selected
end
function weapon:IsVisible()
return self.Visible
end
function weapon:Toggle()
if (self:GetState() == WEAPON_IDLE) then
self:SetState(WEAPON_PUT_AWAY)
else
self.Selected = true
self.Visible = true
self.ViewModel:Show()
self:SetState(WEAPON_TAKE_OUT)
end
end
function weapon:SetAnimating(anim)
self.Animating = anim
end
-- Weapon Functionality
-- Reload
function weapon:CanDoReload()
return (weapon:IsIdle() and weapon:GetPrimaryClip()<weapon:GetPrimaryClipSize())
end
function weapon:Reload()
if (not self:CanDoReload()) then return end
self:SetState(WEAPON_RELOAD)
end
-- Performs actual clip reloading math
function weapon:DoReload()
if (self:GetPrimaryAmmo() >= self:GetPrimaryClipSize()) then
if (self.Zoomed) then self:ToggleIronsights() end
self.Primary.Clip = self:GetPrimaryClipSize()
self.Primary.Ammo = self.Primary.Ammo - self:GetPrimaryClipSize()
elseif (self:GetPrimaryAmmo() > 0) then
if (self.Zoomed) then self:ToggleIronsights() end
self.Primary.Clip = self:GetPrimaryAmmo()
self.Primary.Ammo = 0
end
end
-- Primary Fire Mode
function weapon:GetPrimaryClip()
return self.Primary.Clip
end
function weapon:GetPrimaryClipSize()
return self.Primary.ClipSize
end
function weapon:GetPrimaryAmmo()
return self.Primary.Ammo
end
function weapon:IsPrimaryClipEmpty()
return self.Primary.Clip <= 0
end
function weapon:CanPrimaryFire()
return (AppTime() >= self:GetNextPrimaryFire()) and (not self:IsReloading()) and (not self:IsPrimaryClipEmpty())
end
function weapon:SetNextPrimaryFire(time)
self.Primary.NextFire = time
end
function weapon:GetNextPrimaryFire()
return self.Primary.NextFire
end
function weapon:TakePrimaryAmmo(num)
self.Primary.Clip = self.Primary.Clip - num
end
function weapon:PrimaryFire()
if (not self:CanPrimaryFire()) then
if (self:IsEmpty() and not self:IsReloading()) then self:PlaySound("WEAPON_DRYFIRE") end
if (self.AutoReload and self:GetPrimaryAmmo() > 0) then
self:Reload()
else
return -- We can not perform a primary fire
end
end
self:SetNextPrimaryFire(AppTime() + self.Primary.Delay)
self:SetNextSecondaryFire(AppTime() + self.Secondary.Delay)
self:SetState(WEAPON_FIRE)
self:SetAnimation("WEAPON_FIRE")
self:TakePrimaryAmmo(1)
if (HOST_TIMESCALE == 1.0) then
CreateBullet(self.FireSpot:GetPosition(1), fw.main.camera.mat:K():Scale(100))
else
CreateBullet(self.FireSpot:GetPosition(1), fw.main.camera.mat:K():Scale(50*HOST_TIMESCALE))
end
end
-- Secondary Fire Mode
function weapon:GetSecondaryClip()
return self.Secondary.Clip
end
function weapon:GetSecondaryClipSize()
return self.Secondary.ClipSize
end
function weapon:GetSecondaryAmmo()
return self.Secondary.Ammo
end
function weapon:IsSecondaryClipEmpty()
return self.Secondary.Clip <= 0
end
function weapon:CanSecondaryFire()
return (AppTime() >= self:GetNextSecondaryFire()) and (self:IsIdle()) and (not self:IsSecondaryClipEmpty())
end
function weapon:SetNextSecondaryFire(time)
self.Secondary.NextFire = time
end
function weapon:GetNextSecondaryFire()
return self.Secondary.NextFire
end
function weapon:TakeSecondaryAmmo(num)
self.Secondary.Clip = self.Secondary.Clip - num
end
function weapon:SecondaryFire()
if (not self:CanSecondaryFire()) then return end -- We can not perform a secondary fire
self:SetNextPrimaryFire(AppTime() + self.Primary.Delay)
self:SetNextSecondaryFire(AppTime() + self.Secondary.Delay)
end
-- Melee Fire Mode
function weapon:PrimaryMeleeAttack()
-- TODO: Implement melee state, animation, sounds, and functionality
end
function weapon:SecondaryMeleeAttack()
-- TODO: Implement melee state, animation, sounds, and functionality
end
-- Ironsights / Zoom
function weapon:ToggleIronsights()
if (self:IsPrimaryClipEmpty()) then
self.Zoomed = false
else
if (self.Zoomed) then
self.Zoomed = false
self:SetAnimation("WEAPON_FROM_ZOOM")
else
self.Zoomed = true
self:SetAnimation("WEAPON_TO_ZOOM")
end
end
end
-- Weapons Table Hierarchy
if lastweapon==nil then
firstweapon=weapon
lastweapon=weapon
else
lastweapon.next=weapon
weapon.prev=lastweapon
end
weapons[weapon]=weapon
function weapon:Free()
weapons[self]=nil
end
-- All weapons created from CreateWeapon inherit the base functionality implemented in this file
weapon.Base = weapon
return weapon
end
Questions? Comments? Concerns? etc.?
A demo will be out soon, think within the week-ish. Another video will precede the demo.