A Begginer's First Code - Tank Fighters
BlitzMax Forums/BlitzMax Beginners Area/A Begginer's First Code - Tank Fighters
| ||
| Hello guys, I used to program with Blitz3D a while back. Haven't coded in a while though, since I got into college. This week I found out Blitzmax was free (I didn't use it back then because I had spent my money on B3D) and since it is more up to date than Blitz3D, as far as I know, I wanted to try it. However I'm back to being a beginner (have a good idea of how programming works, but am still a little lost about Blitzmax and the advanced part of OOP). I'm reading the PDF for beginners I found in the forum, currently studying types, and wanted to put what I learned into practice in my first real code (Hello World doesn't count anymore!). I'm posting it so you can be critical about it, maybe point some stuff I could do better (or more OOP inclined). It's just a small tank mini-game (W-A-S-D to move, Tab to shoot, Esc to quit). Thanks guys!
Rem
TANK FIGHTERS EXAMPLE
Just a small tank fighting game
EndRem
Strict 'Now all variables must be declared before used
Const ENEMYTANKWIDTH:Int=10 'Size of tanks
Const ENEMYTANKHEIGHT:Int=20
Const NUMBEROFTANKS:Int=10 'Number of enemies
Global TanksList:TList=CreateList() 'List with enemy tanks (Player will be handled on a local variable)
Global CannonList:TList=CreateList() 'List with all the Tanks Bullets (used to be on the enemy tanks type
'but I needed to acess it from the player type)
'Variables to limit fps
Local LIMIT_FPS=60
Local LIMIT_START 'the time in millisecs in the beginning of the loop
Local LIMIT_LOOPTIME=1000/LIMIT_FPS
Local TempEnemyTank:EnemyTanks 'Temporary variable for making enemy tanks
Graphics 800,600
'We create a player in the middle of the screen
Local P1:Player=New Player
P1.X=GraphicsWidth()/2
P1.Y=GraphicsHeight()/2
P1.Dir=0
'Creating all enemy tanks
Local N:Int
For N=1 To NUMBEROFTANKS
EnemyTanks.CreateTank()
Next
'MAIN LOOP
While Not KeyHit(Key_Escape)
LIMIT_START=MilliSecs()
'Update Tanks
For TempEnemyTank=EachIn TanksList
TempEnemyTank.AimAtPlayer(P1)
TempEnemyTank.Move()
TempEnemyTank.DrawTank()
If TempEnemyTank.CheckBulletsCollisions(P1) Then
P1.Armor:-15
EndIf
Next
If P1.Armor<=0 Then
ClearList(TanksList)
While Not KeyHit(Key_Space)
SetColor(255,0,0)
DrawText("YOU LOSE",GraphicsWidth()/2,GraphicsHeight()/2)
DrawText("Press <Space> to exit",GraphicsWidth()/2,GraphicsHeight()/2+30)
Flip;Cls
Wend
End
EndIf
'Player controls
If KeyDown(Key_W) Then
P1.Move()
ElseIf KeyDown(Key_S) Then
P1.Move(1)
EndIf
If KeyDown(Key_D) Then
P1.Dir:+4
ElseIf KeyDown(Key_A) Then
P1.Dir:-4
EndIf
If KeyDown(Key_Tab) Then
P1.Shoot()
EndIf
Cannon.UpdateBullets()
'Drawing Player and the armor bar
P1.DrawTank()
SetColor(0,0,255)
DrawText("Armor:",10,10)
DrawRect(10,23,P1.Armor,20)
Flip;Cls
'Delay the time necessary to keep the framerate in the limit
If (MilliSecs()-LIMIT_START)<LIMIT_LOOPTIME Then
Delay(LIMIT_LOOPTIME-(MilliSecs()-LIMIT_START))
EndIf
Wend
Type Cannon 'Type for a Cannon bullet
Field X:Float,Y:Float
Field Dir:Float
Field Speed:Float=3
Field Owner:Int=0 '1=Player 0=Enemy
Function UpdateBullets()
Local Bullet:Cannon
For Bullet=EachIn CannonList
Bullet.Move()
Bullet.DrawBullet()
Next
End Function
Method DrawBullet()
SetColor(255,255,0) 'Enemys bullets are yellow
DrawOval(X,Y,2,2)
End Method
Method Move()
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
End Method
Method CheckCollision(X0,Y0,Dist:Float=20) 'Check if bullet collided with target
'We will use a circle collision to simplify
If Sqr((X0-X)^2+(Y0-Y)^2)<=Dist Then 'If bullet collide with target
Return True
Else
Return False
EndIf
End Method
End Type
Type Player 'Player type
Field X:Float
Field Y:Float 'Player's position
Field Speed:Float=2 'Default speed is 0.5
Field Armor:Float=200 'Default player's armor is 200
Field Dir:Float
Field Cooldown:Int=1000
Field Timer
Method Shoot()
If MilliSecs()-Timer>Cooldown Then
Local B:Cannon=New Cannon
B.X=X
B.Y=Y
B.Dir=Dir
B.Speed=8
B.Owner=1
CannonList.AddLast(B)
Timer=MilliSecs()
EndIf
End Method
Method DrawTank() 'Draws the tank
SetColor(0,0,255) 'Player tank color is blue
Rem
SEE ENEMY TANK'S METHOD OF DRAWING FOR MORE DETAILS
EndRem
Local C1:Float[2]
Local C2:Float[2]
Local C3:Float[2]
Local C4:Float[2] 'C1[0]=X and C1[1]=Y
Local CDist:Float,CAng:Float
CDist=Sqr((ENEMYTANKWIDTH/2)^2+(ENEMYTANKHEIGHT/2)^2)
CAng=ATan(Float(ENEMYTANKWIDTH)/Float(ENEMYTANKHEIGHT))
'Calculating the corners X positions
C1[0]=X-CDist*Cos(Dir-CAng)
C2[0]=X-CDist*Cos(Dir+CAng)
C3[0]=X+CDist*Cos(Dir+CAng)
C4[0]=X+CDist*Cos(Dir-CAng)
'Calculating the corners Y positions
C1[1]=Y-CDist*Sin(Dir-CAng)
C2[1]=Y-CDist*Sin(Dir+CAng)
C3[1]=Y+CDist*Sin(Dir+CAng)
C4[1]=Y+CDist*Sin(Dir-CAng)
'Drawing
'Right side
DrawLine(C2[0],C2[1],C4[0],C4[1])
'Left side
DrawLine(C1[0],C1[1],C3[0],C3[1])
'Back
DrawLine(C2[0],C2[1],C1[0],C1[1])
'Front
DrawLine(C3[0],C3[1],C4[0],C4[1])
End Method
Method Move(Reverse:Int=0)
If Not Reverse Then
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
Else
X:-(Speed*Cos(Dir))
Y:-(Speed*Sin(Dir))
EndIf
End Method
End Type
Type EnemyTanks 'EnemyTanks type
Field Armor:Float=100 'Default armor is 100
Field Speed:Float=0.2 'Default speed is 0.2
Field X:Float
Field Y:Float 'Tank's position
Field Dir:Float 'Tank's direction in degrees
Field Cooldown:Int=Rand(500,1200) 'Cool down to shoot again in millisecs
Field Timer:Int 'Timer for calculating time after last shoot
Method CheckBulletsCollisions(A:Player)
Local C:Cannon
For C=EachIn CannonList
If C.Owner=1 And C.CheckCollision(X,Y) Then 'Checks if a player bullet hitted the enemy tank
Armor:-20
CannonList.Remove(C)
TanksList.Remove(Self)
Return False
EndIf
If C.Owner=0 And C.CheckCollision(A.X,A.Y) Then 'Checks if a enemy bullet hitted a player
CannonList.Remove(C)
Return True
EndIf
Next
Return False
End Method
Method Shoot() 'Shoot a bullet
Local B:Cannon=New Cannon
B.X=X
B.Y=Y
B.Dir=Dir
CannonList.AddLast(B)
End Method
Method AimAtPlayer(A:Player) 'Aims at a player
'Considering the 4 Quadrants
If A.Y>Y And A.X<X Then
Dir=180-ATan((A.Y-Y)/(X-A.X))
ElseIf A.Y>Y And A.X>X Then
Dir=ATan((A.Y-Y)/(A.X-X))
ElseIf A.Y<Y And A.X>X Then
Dir=-ATan((Y-A.Y)/(A.X-X))
ElseIf A.Y<Y And A.X<X Then
Dir=180+ATan((Y-A.Y)/(X-A.X))
EndIf
End Method
Method DrawTank() 'Draws the tank
SetColor(255,0,0) 'Enemy tank color is red
Rem
1 _____ 3
| |
|_____|
2 4
4 Tank Corners to be calculated according to the Dir and X,Y
Corner 1 = BackLeft
Corner 2 = BackRight
Corner 3 = FrontLeft
Corner 4 = FrontRight
Since the tank is going to be a rectangle, the distance from the center (X,Y) to the corners
is going to be the same for the 4 corners. We will call it CDist.
The angle between the center point and the corners will also be constant in this case. We will
call it CAng
Basic trigonometry to get the coordinates of the tank lines
EndRem
Local C1:Float[2],C2:Float[2],C3:Float[2],C4:Float[2] 'C1[0]=X and C1[1]=Y
Local CDist:Float,CAng:Float
CDist=Sqr((ENEMYTANKWIDTH/2)^2+(ENEMYTANKHEIGHT/2)^2)
CAng=ATan(Float(ENEMYTANKWIDTH)/Float(ENEMYTANKHEIGHT))
'Calculating the corners X positions
C1[0]=X-CDist*Cos(Dir-CAng)
C2[0]=X-CDist*Cos(Dir+CAng)
C3[0]=X+CDist*Cos(Dir+CAng)
C4[0]=X+CDist*Cos(Dir-CAng)
'Calculating the corners Y positions
C1[1]=Y-CDist*Sin(Dir-CAng)
C2[1]=Y-CDist*Sin(Dir+CAng)
C3[1]=Y+CDist*Sin(Dir+CAng)
C4[1]=Y+CDist*Sin(Dir-CAng)
'Drawing
'Right side
DrawLine(C2[0],C2[1],C4[0],C4[1])
'Left side
DrawLine(C1[0],C1[1],C3[0],C3[1])
'Back
DrawLine(C2[0],C2[1],C1[0],C1[1])
'Front
DrawLine(C3[0],C3[1],C4[0],C4[1])
'If cooldown is over, shoot and give the tank a random cooldown
If (MilliSecs()-Timer)>=Cooldown Then
Shoot()
Cooldown=Rand(3000,5000)
Timer=MilliSecs()
EndIf
End Method
Method Move()
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
End Method
Function CreateTank(X#=-1,Y#=-1,Armor#=100) 'Create an enemy tank and add it to the list
Local T:EnemyTanks=New EnemyTanks
If X#<0 Then
X#=Rand(0,GraphicsWidth())
EndIf 'Random position if X or Y are negative
If Y#<0 Then
Y#=Rand(0,GraphicsHeight())
EndIf
T.X=X#
T.Y=Y#
T.Dir=Rnd(0,360)
T.Timer=MilliSecs()
TanksList.AddLast(T)
End Function
End Type
|
| ||
| I've realized that my code wasn't still the most fit to an OOP program. After finishing reading the PDF for begginers, I noticed I could have used inheritance here, since some of the methods used in EnemyTanks are also used on Player Tanks. I updated the code, so I would have an Class (that could even be Abstract as far as I know) called Tanks, which would have the basic methods related to the tanks, like moving and checking if a bullet hitted the tank. An EnemyTank would derive from that class, having an extra method of Aiming at a player, and the Player type would override the moving method, giving the tank the option of reverse moving (I even think that the overriding wasn't completely necessary, as I could check on the move method through casting if the tank in an enemy or a player, and only reverse if it's a player, but then, I think the overriding way makes it more organized, maybe the purpose of using inheritance itself). As you can see, I'm still struggling a little to adjust to this new paradigm of programming, but I think I'm starting to get the feel of it. One thing I noticed is that you can't change the parameters of the overrided method, so I would have to keep the "Reversal" parameter in the base class method, even though I don't use it there. I'm not sure about this, but I guess you can't override class functions right?
Rem
TANK FIGHTERS EXAMPLE
Just a small tank fighting game
EndRem
SuperStrict 'Now all variables must be declared before used
Const ENEMYTANKWIDTH:Int=10 'Size of tanks
Const ENEMYTANKHEIGHT:Int=20
Const NUMBEROFTANKS:Int=10 'Number of enemies
Global TanksList:TList=CreateList() 'List with all tanks (But player will be handled on a local variable)
Global CannonList:TList=CreateList() 'List with all the Tanks Bullets (used to be on the enemy tanks type
'but I needed to acess it from the player type)
'Variables to limit fps
Local LIMIT_FPS:Int=60
Local LIMIT_START:Int 'the time in millisecs in the beginning of the loop
Local LIMIT_LOOPTIME:Float=1000/Float(LIMIT_FPS)
Local TempTank:Tanks 'Temporary variable for making enemy tanks
Graphics 800,600
'We create a player in the middle of the screen
Local P1:Player=New Player
P1.X=GraphicsWidth()/2
P1.Y=GraphicsHeight()/2
P1.Dir=0
P1.Speed=2
P1.Cooldown=500
TanksList.AddLast(P1)
'Creating all enemy tanks
Local N:Int
For N=1 To NUMBEROFTANKS
EnemyTanks.CreateTank()
Next
'MAIN LOOP
While Not KeyHit(Key_Escape)
LIMIT_START=MilliSecs()
'Update All Tanks
For TempTank=EachIn TanksList
If EnemyTanks(TempTank) Then
Local TempETank:EnemyTanks=EnemyTanks(TempTank)
TempETank.AimAtPlayer(P1) 'I need to use an EnemyTank object since the AimAtPlayer is exclusive of this extended type
TempTank.Move() 'I could use either TempETank Or TempTank right? Since Move is part of the Tank Type
TempTank.Shoot()
EndIf
TempTank.DrawTank()
TempTank.CheckBulletsCollisions()
Next
If P1.Armor<=0 Then
ClearList(TanksList)
While Not KeyHit(Key_Space)
SetColor(255,0,0)
DrawText("YOU LOSE",GraphicsWidth()/2,GraphicsHeight()/2)
DrawText("Press <Space> to exit",GraphicsWidth()/2,GraphicsHeight()/2+30)
Flip;Cls
Wend
End
EndIf
'Player controls
If KeyDown(Key_W) Then
P1.Move()
ElseIf KeyDown(Key_S) Then
P1.Move(1)
EndIf
If KeyDown(Key_D) Then
P1.Dir:+4
ElseIf KeyDown(Key_A) Then
P1.Dir:-4
EndIf
If KeyDown(Key_Tab) Then
P1.Shoot()
EndIf
Cannon.UpdateBullets()
'Drawing the armor bar
SetColor(0,0,255)
DrawText("Armor:",10,10)
DrawRect(10,23,P1.Armor,20)
Flip;Cls
'Delay the time necessary to keep the framerate in the limit
If (MilliSecs()-LIMIT_START)<LIMIT_LOOPTIME Then
Delay(LIMIT_LOOPTIME-(MilliSecs()-LIMIT_START))
EndIf
Wend
Type Tanks 'EnemyTanks type
Field Armor:Float=100 'Default armor is 100
Field Speed:Float=0.2 'Default speed is 0.2
Field X:Float
Field Y:Float 'Tank's position
Field Dir:Float 'Tank's direction in degrees
Field Cooldown:Int=Rand(500,1200) 'Cool down to shoot again in millisecs
Field Timer:Int 'Timer for calculating time after last shoot
Method CheckBulletsCollisions()
Local C:Cannon
For C=EachIn CannonList
If EnemyTanks(Self) Then
If C.Owner=1 And C.CheckCollision(X,Y) Then 'Checks if a player bullet hitted the enemy tank
Armor:-20
CannonList.Remove(C)
If Armor<=0 Then
TanksList.Remove(Self)
EndIf
EndIf
ElseIf Player(Self) Then
If C.Owner=0 And C.CheckCollision(X,Y) Then 'Checks if a enemy bullet hitted a player
Armor:-20
CannonList.Remove(C)
EndIf
EndIf
Next
End Method
Method Shoot() 'Shoot a bullet
If (MilliSecs()-Timer)>Cooldown Then
Local B:Cannon=New Cannon
B.X=X
B.Y=Y
B.Dir=Dir
If Player(Self) Then
B.Owner=1
B.Speed=3
EndIf
CannonList.AddLast(B)
If EnemyTanks(Self) Then Cooldown=Rand(3000,6000)
Timer=MilliSecs()
EndIf
End Method
Method DrawTank() 'Draws the tank
If Player(Self) Then
SetColor(0,0,255) 'Player tank is blue
Else
SetColor(255,0,0) 'Enemy tank color is red
EndIf
Rem
1 _____ 3
| |
|_____|
2 4
4 Tank Corners to be calculated according to the Dir and X,Y
Corner 1 = BackLeft
Corner 2 = BackRight
Corner 3 = FrontLeft
Corner 4 = FrontRight
Since the tank is going to be a rectangle, the distance from the center (X,Y) to the corners
is going to be the same for the 4 corners. We will call it CDist.
The angle between the center point and the corners will also be constant in this case. We will
call it CAng
Basic trigonometry to get the coordinates of the tank lines
EndRem
Local C1:Float[2],C2:Float[2],C3:Float[2],C4:Float[2] 'C1[0]=X and C1[1]=Y
Local CDist:Float,CAng:Float
CDist=Sqr((ENEMYTANKWIDTH/2)^2+(ENEMYTANKHEIGHT/2)^2)
CAng=ATan(Float(ENEMYTANKWIDTH)/Float(ENEMYTANKHEIGHT))
'Calculating the corners X positions
C1[0]=X-CDist*Cos(Dir-CAng)
C2[0]=X-CDist*Cos(Dir+CAng)
C3[0]=X+CDist*Cos(Dir+CAng)
C4[0]=X+CDist*Cos(Dir-CAng)
'Calculating the corners Y positions
C1[1]=Y-CDist*Sin(Dir-CAng)
C2[1]=Y-CDist*Sin(Dir+CAng)
C3[1]=Y+CDist*Sin(Dir+CAng)
C4[1]=Y+CDist*Sin(Dir-CAng)
'Drawing
'Right side
DrawLine(C2[0],C2[1],C4[0],C4[1])
'Left side
DrawLine(C1[0],C1[1],C3[0],C3[1])
'Back
DrawLine(C2[0],C2[1],C1[0],C1[1])
'Front
DrawLine(C3[0],C3[1],C4[0],C4[1])
End Method
Method Move(Reverse:Int=0)
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
End Method
Function CreateTank(X#=-1,Y#=-1,Armor#=100) 'Create an enemy tank and add it to the list
Local T:EnemyTanks=New EnemyTanks
If X#<0 Then
X#=Rand(0,GraphicsWidth())
EndIf 'Random position if X or Y are negative
If Y#<0 Then
Y#=Rand(0,GraphicsHeight())
EndIf
T.X=X#
T.Y=Y#
T.Dir=Rnd(0,360)
T.Timer=MilliSecs()
TanksList.AddLast(T)
End Function
End Type
Type Cannon 'Type for a Cannon bullet
Field X:Float,Y:Float
Field Dir:Float
Field Speed:Float=3
Field Owner:Int=0 '1=Player 0=Enemy
Function UpdateBullets()
Local Bullet:Cannon
For Bullet=EachIn CannonList
Bullet.Move()
Bullet.DrawBullet()
Next
End Function
Method DrawBullet()
SetColor(255,255,0) 'Enemys bullets are yellow
DrawOval(X,Y,2,2)
End Method
Method Move()
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
End Method
Method CheckCollision:Int(X0:Float,Y0:Float,Dist:Float=20) 'Check if bullet collided with target
'We will use a circle collision to simplify
If Sqr((X0-X)^2+(Y0-Y)^2)<=Dist Then 'If bullet collide with target
Return True
Else
Return False
EndIf
End Method
End Type
Type Player Extends Tanks'Player type
Method Move(Reverse:Int=0) 'Method override - Player can move reverse too
If Not Reverse Then
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
Else
X:-(Speed*Cos(Dir))
Y:-(Speed*Sin(Dir))
EndIf
End Method
End Type
Type EnemyTanks Extends Tanks 'EnemyTanks type
Method AimAtPlayer(A:Player) 'Aims at a player
'Considering the 4 Quadrants
If A.Y>Y And A.X<X Then
Dir=180-ATan((A.Y-Y)/(X-A.X))
ElseIf A.Y>Y And A.X>X Then
Dir=ATan((A.Y-Y)/(A.X-X))
ElseIf A.Y<Y And A.X>X Then
Dir=-ATan((Y-A.Y)/(A.X-X))
ElseIf A.Y<Y And A.X<X Then
Dir=180+ATan((Y-A.Y)/(X-A.X))
EndIf
End Method
End Type
|
| ||
| I did not check your whole code but saw some parts which are improveable. See: Method CheckBulletsCollisions() Local C:Cannon For C=EachIn CannonList If EnemyTanks(Self) Then If C.Owner=1 And C.CheckCollision(X,Y) Then 'Checks if a player bullet hitted the enemy tank Armor:-20 CannonList.Remove(C) If Armor<=0 Then TanksList.Remove(Self) EndIf EndIf ElseIf Player(Self) Then If C.Owner=0 And C.CheckCollision(X,Y) Then 'Checks if a enemy bullet hitted a player Armor:-20 CannonList.Remove(C) EndIf EndIf Next End Method There is no need to have "Type Tank" to know about the classes/types EnemyTank and Player (also you - in this case - check the "self"-object for each cannon, instead to check once, and then loop over every cannon)... replace it with the following code Method CheckBulletsCollisions() abstract and then add an individual method (overriding the base one) for each extending class (EnemyTanks and Player) 'EnemyTanks Method CheckBulletsCollisions() For local C:Cannon = EachIn CannonList If C.Owner=1 And C.CheckCollision(X,Y) Then 'Checks if a player bullet hitted the enemy tank Armor:-20 CannonList.Remove(C) If Armor<=0 Then TanksList.Remove(Self) EndIf EndIf Next End Method But even then you have too additional problems: - you loop over all cannons - even if the armor is already below 0 (-> after a potential tanksList.remove(...) you should return/exit the loop - also you should skip the checks if already starting with armor < 0) - you modify "CannonList" while iterating over it (you should add the "to delete" items to another array/list - and after finishing the CannonList-Loop, delete all "to delete"-items in another loop. Why? If you manipulate the list content you might end up skipping some items "randomly"). OK: for bla in list 'update next not OK: for bla in list 'remove bla from list next OK: for bla in list 'removeList.AddLast(bla) next for bla in removeList 'list.Remove(bla) next BTW: I modified the lists for years without trouble - but some months ago the problems started and that is why Brucey added some notification to his BMX NG-compiler as this should be avoided bydeveloper.s bye Ron |
| ||
| Hello Ron, thanks a lot for the feedback! You're right, in a bigger code we can't afford to check on every type through casting in a method, much rather override it on each extended type. Just to confirm: When a function is abstract, we still need it to have the same parameters and return type when overriding it right? You're also right about exiting the loop after reaching Armor<=0, no need to check the other bullets collision if the tank is already "dead". I just got confused with why skipping the collision check if the armor is below 0. That's because when it reachs 0 or less, the tank is removed from the list, and as far as I understood the object will be deleted since no variable is referring to it anymore. So after that loop it would be impossible to have a tank with 0 or less armor (at least the Enemy Tank, which removes itself right after the armor reachs 0 or less). About avoiding modifying the list while iterating it, it has to do with how the objects organize themselfs in the list? I didn't understand it completely, but it seems like the position of one object on the list is actually a relative position to another object, right? So editing it while you're still going through it could make some mess with the references or something. I'll avoid doing it anyways. Will make some corrections to the code and post it here later! |
| ||
| When abstracting / overriding you need to have the same params, the return type might differ. Eg. I have "Method Init:returntype(params)" for multiple variants of GUI Objects - and within these types I return the individual types without problems - as long as the params keep the same. All these types extend from a base type, did not check if this is the reason. @Tank removal "self" is in scope during the whole method call, so C.CheckCollision(X,Y) will still access "self.x" correctly. BUT ... in this case "tanksList.remove(self)" is absolutely no problem - as you do not iterate over the tanksList. You just do not take care whether the tank is alive/operating or not - that's the only problem with the tank. If you cannot "return/exit" out of a loop (eg. you have multiple objects which might be "dead"): 'loop over all "gameobjects" contained in the blaList (other objects are skipped - except they extend from "gameobject") for local bla:gameobject = eachin blaList if bla.isDead() then continue 'skip to the next entry 'doSomething to living bla objects next @modifying while iterating The list contains "TLink"-objects, they contain links to their neighbours and the object. When removing an object, the corresponding TLink is removed, and the neighbours are adjusted (at least I think so). I also did not properly understand, why this could be problematic (when doing unthreaded-builds) but somehow this is not the problem, but the "iterator" (the object taking care of "what comes next"). It might get more obvious if you add something to a list, while iterating. Imagine you are iterating over a list of: A B F H X Z When reaching "F" in a for-loop, you add "C" and "D" to the list - what happens then? are they then available in the very same for loop - if yes, when? What happens, if you add "M" or "Y" then ? A simple approach is to thave something in the likes of: for bla = Eachin blaList.Copy() 'iterate over the copy if bla.isDead() then blaList.Remove() Next BUT - if you do not remove an object (or modify the blaList itself), you could have saved the "copy()" operation which is the more expensive, the more objects are contained in the list. If you expect to have not that much objects for removal, you might start with an empty array. Then add the "to delete" objects - and if _after_ the loop, that array contains elements, you will iterate over these elements and remove them from the list (so you get 2 for loops: the "update" one, and the "removal" one). We do not have the "modification while iterating over it" problem with that copied list then because we do not delete from that list later on - we remove from the original list and keep the copy "intact" (clearing them at the end). You might think of using some kind of "adjust when needed" approach (like events).
Type TMyType
Field alive:int
Field armor:int = 100
Global entries:TList = CreateList()
Global removeEntries:TList = CreateList()
Method Die()
if not alive then return
if not removeEntries.contains(self) then removeEntries.AddLast(self)
End Method
Method Update()
'do something
'check for death
if amor < 0 then Die()
End Method
Function UpdateAll()
'update all
for local m:TMyType = Eachin entries
m.Update()
'when doing "cross checks" to other entries here
'you might use "obj.alive" to check whether to
'ignore that dead object or not.
'alternatively you could check if "obj" is contained
'in the removeEntries-list
next
'remove dead ones
if removeEntries.Count() > 0
for local m:TMyType = Eachin removeEntries
entries.remove(m)
next
'keep that list clean and remove last links to dead entries
removeEntries.Clear()
endif
End Function
End Type
This "if armor < 0 then Die()" could become even more "event like" if you have some kind of "Method SetArmor()" and only check for "has to die" there - so effectively you only "Die()" when manually calling it - or if a tanks armor gets below 0. For now the armor-check is done on each update - while it then would only be done, if someone hit the object (manipulates the armor). bye Ron |
| ||
| Ron, I have to thank you. All your critics were taken into account and I guess I have a much better code right now than before! I'll list some of the changes here: 1 - I created a "Garbage Collector" to avoid editing a list while iterating it. All objects that need to be deleted are added to a list, then if the object is found in any of the lists it's removed. At first I forgot at all about the Delete list, and just kept the object there. Then it ocurred to me that if there's still a link to it in the delete list, the object still exists. So I took your hint and made the garbage collector iteration through a copy. 2 - I created a Tanks (Base type) function called UpdateAll(), which goes through every tank object and calls its Update() Method, an abstract method that differs on each extended type. 3 - Overrided the New() method on the Player type, so I could make some initializations there (unecessary, but good to get used to it). 4 - Made the CheckBulletsCollisions() and Shoot() methods abstract, overriding them on each extended type to make them adequate to the object being handled (no need to check through casts now). I only didn't do it with the DrawTank() method because it seemed like a waste of resource, when the only thing that changes in the whole method is the color being set on each extended type.
Rem
TANK FIGHTERS EXAMPLE
Just a small tank fighting game
EndRem
SuperStrict 'Now all variables must be declared before used
Const ENEMYTANKWIDTH:Int=10 'Size of tanks
Const ENEMYTANKHEIGHT:Int=20
Const NUMBEROFTANKS:Int=10 'Number of enemies
'List with all tanks (Even though the player tank will be handled on a local variable -> Easier and Faster?)
Global TanksList:TList=CreateList()
'List with all the Tanks Bullets (used to be on the enemy tanks type but I needed To acess it from the player Type)
Global CannonList:TList=CreateList() 'MAKE IT A FIELD OF THE TANKS TYPE? - Is it worth it? Seems pointless
'List with all objects that need to be deleted - Made to avoid the simultaneous editing And iterating of the lists
Global ToDeleteList:TList=CreateList()
'Variables to limit fps
Local LIMIT_FPS:Int=60
Local LIMIT_START:Int 'The time in millisecs in the beginning of the loop
Local LIMIT_LOOPTIME:Float=1000/Float(LIMIT_FPS) 'The time a loop "should" have
'Temporary variable for making and handling tanks
Local TempTank:Tanks
Graphics 800,600
'We create a player in the middle of the screen and associate it with the P1 variable for easy and fast acess
'Instead of initializing the fields of player here, we override the new method of its type
'Note: Since the New method doesn't takes parameters and the returned values must be ignored
'we can't use it to initialize fields that aren't going to be the same on every player everytime (X,Y and Dir for example)
Global P1:Player=New Player 'We use global here so we can use this variable on methods (Like EnemyTanks.Update())
P1.X=GraphicsWidth()/2
P1.Y=GraphicsHeight()/2
P1.Dir=0
'Creating all enemy tanks
Local N:Int
For N=1 To NUMBEROFTANKS
EnemyTanks.CreateEnemyTank()
Next
'MAIN LOOP
While Not KeyHit(Key_Escape)
LIMIT_START=MilliSecs()
'Check if the player losed (NO PLACE FOR LOSERS HERE!)
If P1.Armor<=0 Then
ClearList(TanksList)
While Not KeyHit(Key_Space)
SetColor(255,0,0)
DrawText("YOU LOSE",GraphicsWidth()/2,GraphicsHeight()/2)
DrawText("Press <Space> to exit",GraphicsWidth()/2,GraphicsHeight()/2+30)
Flip;Cls
Wend
End
EndIf
'The Tanks function UpdateAll goes through all tanks objects and calls the method Update, which is
'an abstract tanks method, overrided on the enemy tanks and player tanks so they do the necessary
'update on their instances.
Tanks.UpdateAll() 'Update all tanks
Cannon.UpdateBullets()
'Drawing the armor bar
SetColor(0,0,255)
DrawText("Armor:",10,10)
DrawRect(10,23,P1.Armor*3,20)
Flip;Cls
'Checks every list for objects on the ToDeleteList and delete them is present
For Local D:Object=EachIn ToDeleteList.Copy()
If TanksList.FindLink(D) Then
TanksList.Remove(D)
EndIf
If CannonList.FindLink(D) Then
CannonList.Remove(D)
EndIf
'Removes the object from the delete list, since if it's still there, the object itself won't be deleted
ToDeleteList.Remove(D)
Next
'Delays the time necessary to keep the framerate in the limit
If (MilliSecs()-LIMIT_START)<LIMIT_LOOPTIME Then
Delay(LIMIT_LOOPTIME-(MilliSecs()-LIMIT_START))
EndIf
Wend
Type Tanks 'EnemyTanks type
Field Armor:Float=100 'Default armor is 100
Field Speed:Float=0.2 'Default speed is 0.2
Field X:Float
Field Y:Float 'Tank's position
Field Dir:Float 'Tank's direction in degrees
Field Cooldown:Int=Rand(500,1200) 'Cool down to shoot again in millisecs (Default->Random between 0.5-1.2s)
Field Timer:Int 'Timer for calculating time after last shoot (Cooldown)
Method CheckBulletsCollisions() Abstract 'Method for checking collision with bullets
Method Shoot() Abstract 'Shoot a bullet
Method Update() Abstract 'Updates singular tank
'I didn't think necessary to Abstract this method, since the only conditional event is a different
'color being set depending on the Self type.
Method DrawTank() 'Draws the tank
If Player(Self) Then
SetColor(0,0,255) 'Player tank is blue
Else
SetColor(255,0,0) 'Enemy tank color is red
EndIf
Rem
1 _____ 3
| |
|_____|
2 4
4 Tank Corners to be calculated according to the Dir and X,Y
Corner 1 = BackLeft
Corner 2 = BackRight
Corner 3 = FrontLeft
Corner 4 = FrontRight
Since the tank is going to be a rectangle, the distance from the center (X,Y) to the corners
is going to be the same for the 4 corners. We will call it CDist.
The angle between the center point and the corners will also be constant in this case. We will
call it CAng
Basic trigonometry to get the coordinates of the tank lines
EndRem
Local C1:Float[2],C2:Float[2],C3:Float[2],C4:Float[2] 'C1[0]=X and C1[1]=Y
Local CDist:Float,CAng:Float
CDist=Sqr((ENEMYTANKWIDTH/2)^2+(ENEMYTANKHEIGHT/2)^2)
CAng=ATan(Float(ENEMYTANKWIDTH)/Float(ENEMYTANKHEIGHT))
'Calculating the corners X positions
C1[0]=X-CDist*Cos(Dir-CAng)
C2[0]=X-CDist*Cos(Dir+CAng)
C3[0]=X+CDist*Cos(Dir+CAng)
C4[0]=X+CDist*Cos(Dir-CAng)
'Calculating the corners Y positions
C1[1]=Y-CDist*Sin(Dir-CAng)
C2[1]=Y-CDist*Sin(Dir+CAng)
C3[1]=Y+CDist*Sin(Dir+CAng)
C4[1]=Y+CDist*Sin(Dir-CAng)
'Drawing
'Right side
DrawLine(C2[0],C2[1],C4[0],C4[1])
'Left side
DrawLine(C1[0],C1[1],C3[0],C3[1])
'Back
DrawLine(C2[0],C2[1],C1[0],C1[1])
'Front
DrawLine(C3[0],C3[1],C4[0],C4[1])
End Method
'Method for moving tank (Default is moving just forward. Only the player can move backwards
'so we override this method on the player type)
Method Move(Reverse:Int=0)
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
End Method
Function UpdateAll()
'Update All Tanks according to its type
For Local Temp:Tanks=EachIn TanksList
Temp.Update()
Next
End Function
End Type
Type Cannon 'Type for a Cannon bullet
Field X:Float,Y:Float
Field Dir:Float
Field Speed:Float=3 'Default speed (Players is faster)
Field Owner:Int=0 '1=Player 0=Enemy
Function UpdateBullets()
Local Bullet:Cannon
For Bullet=EachIn CannonList
Bullet.Move()
Bullet.DrawBullet()
Next
End Function
Method DrawBullet()
SetColor(255,255,0) 'Bullets are yellow
DrawOval(X,Y,2,2)
End Method
Method Move()
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
End Method
Method CheckCollision:Int(X0:Float,Y0:Float,Dist:Float=20) 'Check if bullet collided with target
'We will use a circle collision to simplify
If Sqr((X0-X)^2+(Y0-Y)^2)<=Dist Then 'If bullet collide with target
Return True
Else
Return False
EndIf
End Method
End Type
Type Player Extends Tanks'Player type
Method Move(Reverse:Int=0) 'Method override - Player can move reverse too
If Not Reverse Then
X:+(Speed*Cos(Dir))
Y:+(Speed*Sin(Dir))
Else
X:-(Speed*Cos(Dir))
Y:-(Speed*Sin(Dir))
EndIf
End Method
Method CheckBulletsCollisions()
Local C:Cannon
For C=EachIn CannonList
If C.Owner=0 And C.CheckCollision(X,Y) Then 'Checks if a enemy bullet hitted a player
Armor:-20
ToDeleteList.AddLast(C)
'CannonList.Remove(C)
EndIf
Next
End Method
Method Shoot() 'Shoot a bullet
If (MilliSecs()-Timer)>Cooldown Then
Local B:Cannon=New Cannon
B.X=X
B.Y=Y
B.Dir=Dir
B.Owner=1
B.Speed=3
CannonList.AddLast(B)
Timer=MilliSecs()
EndIf
End Method
Method New() 'Overriding new method so we can initialize some fields of our player tank here
Speed=2
Cooldown=500
TanksList.AddLast(Self)
End Method
Method Update()
'Update player Tanks
DrawTank()
CheckBulletsCollisions()
'Player controls
If KeyDown(Key_W) Then
Move()
ElseIf KeyDown(Key_S) Then
Move(1) '1=Reverse movement
EndIf
If KeyDown(Key_D) Then
Dir:+4
ElseIf KeyDown(Key_A) Then
Dir:-4
EndIf
If KeyDown(Key_Tab) Then
Shoot()
EndIf
End Method
End Type
Type EnemyTanks Extends Tanks 'EnemyTanks type
Method AimAtPlayer(A:Player) 'Aims at a player
'Considering the 4 Quadrants
If A.Y>Y And A.X<X Then
Dir=180-ATan((A.Y-Y)/(X-A.X))
ElseIf A.Y>Y And A.X>X Then
Dir=ATan((A.Y-Y)/(A.X-X))
ElseIf A.Y<Y And A.X>X Then
Dir=-ATan((Y-A.Y)/(A.X-X))
ElseIf A.Y<Y And A.X<X Then
Dir=180+ATan((Y-A.Y)/(X-A.X))
EndIf
End Method
Method CheckBulletsCollisions()
Local C:Cannon
For C=EachIn CannonList
If C.Owner=1 And C.CheckCollision(X,Y) Then 'Checks if a player bullet hitted the enemy tank
Armor:-20
CannonList.Remove(C)
If Armor<=0 Then
ToDeleteList.AddLast(Self) 'Adds the Tank to the garbage can!
'TanksList.Remove(Self)
Exit
EndIf
EndIf
Next
End Method
Method Shoot() 'Shoot a bullet
If (MilliSecs()-Timer)>Cooldown Then
Local B:Cannon=New Cannon
B.X=X
B.Y=Y
B.Dir=Dir
CannonList.AddLast(B)
Cooldown=Rand(3000,6000)
Timer=MilliSecs()
EndIf
End Method
Method Update()
'Update Enemy Tanks
AimAtPlayer(P1)
Move()
Shoot()
DrawTank()
CheckBulletsCollisions()
End Method
Function CreateEnemyTank(X#=-1,Y#=-1,Armor#=100) 'Create an enemy tank and add it to the list
Local T:EnemyTanks=New EnemyTanks
If X#<0 Then
X#=Rand(0,GraphicsWidth())
EndIf 'Random position if X or Y are negative
If Y#<0 Then
Y#=Rand(0,GraphicsHeight())
EndIf
T.X=X#
T.Y=Y#
T.Dir=Rnd(0,360)
T.Timer=MilliSecs()
TanksList.AddLast(T)
End Function
End Type
About the armor and the Die() method: Even though I believe it would be much more organized the way you suggest (I plan on changing the code further), the only moment where I change the Tank Armor is during the collision check. Right after I change it I check if it's below or equal to 0 and delete the tank instance in case it is. In the players case I check in the beggining of the loop and finish the game already if it is below 0. So the issue of going through tanks that are already dead doesn't happen in the collision check rountine at least, but I'll review the code further later to confirm it! Thanks Again Ron, Ian |
| ||
| Sorry guyz if you think I'm raising the dead, but this is a great game and some good programming besides, lots of REMarks to explain the code. I like how the enemies directly face the player and shoot in the same direction too. This also runs out of the box, no need to download extra LIBS. The vectors remind me considerably of Armor Attack: ![]() Good job, Ian ! I would definitely want to see you finish writing this game - w source to explain your wizardry. :) |
| ||
| It's missing a "Timer=MilliSecs()" in the New Method of the player. > If Millisecs() on the computer returns negative value, the player can't shoot. |
| ||
| I die so quickly once I ran the game, I didn't notice. Nor did I know that you could fight back. Let me see the code ... Okay, I see MilliSecs() in use. Is he doing this to stagger the times of animations ? Yeah - that's not so good as it can go <0 Just use Flip(), fire and forget for your timers. :) |
| ||
| Hello guys! Sorry for taking long to reply, these days have been a little rush, I'm about to start on a new job and move, so you can imagine how crazy things are right now haha Thanks for the compliment, I'm still not really used to the OOP programming, neither Blitzmax language, so I feel happy to hear that this is a good code. The Millisecs() is used to time the shoots, so there's a cooldown everytime an enemy or a player shoots (on the enemies the cooldown is different everytime, the motive for this is not having all the tanks shooting at the same time, and making it a little less predictable). I didn't know Millisecs() could return a negative value, but is this really a problem to the code? Because the expression "Millisecs()-Timer" will return the difference between the time now and the time when the last shoot was made. Think it would work even with negative values, like say -100 on Timer and -50 on current Millisecs (-50)-(-100)=50. However, there are several things that can be improved in that code, like the framerate limiting: I don't like the idea of keeping a delay in the main loop. Instead, I think I should make a check on anything that depends on the framerate (tanks movement, the drawings and the flip) and everything else should be run on every loop, despite the framerate limit. This would make the whole program more efficient. When I sort things out, about this new job and moving, I think I'll make some changes in that code, and improve it. Maybe making a tiled map, with some Astar path finding. I think theres potential for growth there :) |
| ||
| Just a quick word. You =DO= have the command called SetRotation, Ian. You could draw a white rectangle, save it to an image, then use SetRotation and SetColor to plot both the player and enemies with a lot less calculations: Strict
Graphics 800,600
DrawRect 0,0,34,3 ' color is white by default, 1st tread
DrawRect 0,27,34,3 ' 2nd tread
DrawRect 4,5,24,20 ' center box is filled
SetColor 0,0,0 ' set color to BLACK
DrawRect 6,7,20,16 ' erase a little inside that center box
SetColor 255,255,255 ' return back to WHITE
DrawRect 15,12,30,6 ' draw the cannon
AutoMidHandle 1 ' all images created will have a natural center
Global img_tank:TImage=CreateImage(42,30)
GrabImage img_tank,0,0 ' grab image above and save to TIMAGE
Global x#=400,y#=300,r ' need REAL numbers for position
Repeat
Cls
SetRotation r ' rotate any image drawn after this
DrawImage img_tank,x#,y#
If KeyDown(key_left) Then r:-4 ' rotate left
If KeyDown(key_right) Then r:+4 ' rotate right
If KeyDown(key_up) ' move forward based on rotation
x#:+Cos(r)*2.0 ' it is NECESSARY to have decimal zero attached to your
y#:+Sin(r)*2.0 ' integer calculations to ensure real number results return
EndIf
Flip
Until KeyDown(key_escape) ' exit on ESCAPE |
| ||
| dw817, Thanks! I didn't know much about images on Blitzmax when I started the code and relied on the basic drawing functions, so I had to do the rotation calculations. But making a image and drawing on it is indeed much better. Not only you avoid making the calculations but you can make much better sprites on the fly! Will GrabImage "grab" a portion of the backbuffer with the width and height of the image, taking the point given as a reference? So in this case it would take a 42x30 square starting on 0,0? I'll try to incorporate this to the code! :) |
