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! :) |