A Begginer's First Code - Tank Fighters

BlitzMax Forums/BlitzMax Beginners Area/A Begginer's First Code - Tank Fighters

Ian Caio(Posted 2015) [#1]
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



Ian Caio(Posted 2015) [#2]
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



Derron(Posted 2015) [#3]
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


Ian Caio(Posted 2015) [#4]
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!


Derron(Posted 2015) [#5]
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


Ian Caio(Posted 2015) [#6]
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


dw817(Posted 2016) [#7]
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. :)


Bobysait(Posted 2016) [#8]
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.


dw817(Posted 2016) [#9]
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. :)


Ian Caio(Posted 2016) [#10]
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 :)


dw817(Posted 2016) [#11]
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



Ian Caio(Posted 2016) [#12]
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! :)