wie hier angekündigt, habe ich mir mal erlaubt, die Idee eines KI-Wettbewerbs nach Blitzmax zu portieren und da die vorzüge von OOP zu nutzen.
Update 0.1: Respawn-System (Danke an ChaosCoder)
Als erstes der Code, für die Kopierfaulen unten noch ein Link zu einer Zip-Datei, welche alle 4 dateien enthält:
gameBase.bmx
BlitzMax: [AUSKLAPPEN] [EINKLAPPEN] SuperStrict Const ACCELERATION:Float = 0.1 Const BULLET_DAMAGE:Int = 1 Const BULLET_DELAY:Int = 1000 Const BULLET_SPEED:Int = 6 Const DISTANCE_TO_HIT:Int = 8 Const MAX_SPEED:Int = 4 Const MAX_SPEED_BACKWARD:Int = 2 Const MAX_TURN_SPEED:Int = 3 Const PLAYER_HEALTH:Int = 3 Const TIME_RESPAWN:Int = 1000
Type TGameField Field player:TList = CreateList() Field width:Int Field height:Int Field bullets:TList = CreateList() Method update() For Local p:TPlayer = EachIn player If p.isDead() And p._deadtime+TIME_RESPAWN < MilliSecs() p._respawn() If p.isDead() Continue p._unlock() p.update() p._update() p._lock() If p._x < 0 p._x = 0 If p._y < 0 p._y = 0 If p._x > width p._x = width If p._y > height p._y = height Next For Local b:TBullet = EachIn bullets b._update() If b._x < 0 Or b._y < 0 Or b._x > width Or b._y > height bullets.remove(b) Next For Local b:TBullet = EachIn bullets For Local p:TPlayer = EachIn player If p.isDead() Continue If Sqr((b._x-p._x)^2 + (b._y-p._y)^2) <= DISTANCE_TO_HIT And Not (b._orig = p) bullets.remove(b) p._health:-BULLET_DAMAGE If p._health <= 0 p._die(b) End If Next Next End Method Method setSize(w:Int, h:Int) Self.width = w Self.height = h End Method Method addPlayer(p:TPlayer) Self.player.addLast(p) p._field = Self; End Method Method addBullet(b:TBullet) Self.bullets.addLast(b) End Method End Type
Type TPlayer Abstract Field _locked:Int=True Field _health:Int = PLAYER_HEALTH Field _x:Float Field _y:Float Field _ri:Float Field _speed:Float Field _field:TGameField Field _lastshoot:Long Field _turnlock:Int = False Field _speedLock:Int = False Field _kills:Int = 0 Field _dead:Int Field _deadtime:Int Field _deaths:Int Method _update() Final _x:+Cos(_ri) * _speed _y:-Sin(_ri) * _speed End Method Method _lock() Final _locked=True End Method Method _unlock() Final _locked=False _turnlock=False _speedlock=False End Method Method _respawn() Final _dead = 0 _health:Int = PLAYER_HEALTH _x = Rand(0, 550) _y = Rand(0, 550) _ri = Rand(0, 360) _speed = 0 respawn() End Method Method _die(bullet:TBullet) Final bullet._orig._kills:+1 _Deaths:+1 _dead = 1 _deadtime = MilliSecs() End Method Method getX:Float() Final Return _x End Method Method getY:Float() Final Return _y End Method Method getRi:Float() Final Return _ri End Method Method getSpeed:Float() Final Return _speed End Method Method getKills:Int() Final Return _kills End Method Method getDeaths:Int() Final Return _deaths End Method Method getScore:Int() Final Return getKills()-getDeaths() End Method Method getHealth:Int() Final Return _health End Method Method isDead:Int() Final Return _dead End Method Method getOtherPlayers:TList() Final Local ret:TList = _field.player.copy() ret.remove(Self) Return ret End Method
Method getBullets:TList() Final Return _field.bullets.copy() End Method Method update() Abstract Method turn(r:Int) Final If _locked Or _turnlock Return If Abs(r) > MAX_TURN_SPEED r = MAX_TURN_SPEED * Sgn(r) _ri:+r _ri = (_ri + 360) Mod 360 _turnlock = True; End Method Method speedUp() Final If _locked Or _speedlock Return If _speed>=MAX_SPEED _speed=MAX_SPEED Return End If _speed :+ACCELERATION _speedlock = True End Method Method speedDown() Final If _locked Or _speedlock Return If _speed <= -MAX_SPEED_BACKWARD _speed = -MAX_SPEED_BACKWARD Return End If _speedlock=True _speed :-ACCELERATION End Method Method break() Final If Abs(_speed) < ACCELERATION _speed = 0 If _speed > 0 speedDown() Else If _speed < 0 speedUp(); End If End Method Method shoot() Final If _locked Return If(MilliSecs()-_lastshoot) < BULLET_DELAY Return; Local b:TBullet = New TBullet b._orig = Self b._x = _x b._y = _y b._ri = _ri b._speed = BULLET_SPEED _field.addBullet(b) _lastshoot = MilliSecs() End Method Method respawn() End Method End Type
Type TBullet Field _x:Float Field _y:Float Field _ri:Float Field _speed:Int Field _orig:TPlayer Method _update() Final _x:+Cos(_ri) * _speed _y:-Sin(_ri) * _speed End Method Method getOrig:TPlayer() Final Return _orig End Method Method getX:Int() Final Return _x End Method Method getY:Int() Final Return _y End Method Method getRi:Int() Final Return _ri End Method Method getSpeed:Int() Final Return _speed End Method End Type
Type TGameRenderer Abstract Field gameField:TGameField Method draw(x:Int, y:Int) Abstract End Type
SampleRenderer.bmx
BlitzMax: [AUSKLAPPEN] [EINKLAPPEN] Import "GameBase.bmx" SuperStrict
Type TPlayerToColor Field player:TPlayer Field r:Int Field g:Int Field b:Int End Type
Type TSampleGameRenderer Extends TGameRenderer Global colors:Int[] = [255,0,0, 0,255,0, 0,0,255, 0,255,255, 255,0,255, 255,255,0] Field colorMap:TList = CreateList() Field colorIDX:Int = 0; Method draw(x:Int, y:Int) SetOrigin x,y SetColor 255, 255, 255 DrawLine 0, 0, gameField.width, 0 DrawLine 0,0,0,gamefield.height DrawLine 0,gamefield.height,gamefield.width,gamefield.height DrawLine gamefield.width, 0, gamefield.width, gamefield.height Local i:Int For Local p:TPlayer = EachIn Self.gameField.player setColorForPlayer p DrawText p.getKills(), -85, 0+i*16 DrawText p.getDeaths(), -55, 0+i*16 DrawText p.getScore(), -25, 0+i*16 If Not p.isDead() Local px1:Int = p._x+Cos(p._ri )*10 Local py1:Int = p._y-Sin(p._ri )*10 Local px2:Int = p._x+Cos(p._ri - 150)*10 Local py2:Int = p._y-Sin(p._ri - 150)*10 Local px3:Int = p._x+Cos(p._ri + 150)*10 Local py3:Int = p._y-Sin(p._ri + 150)*10 DrawLine px1, py1, px2, py2 DrawLine px2, py2, px3, py3 DrawLine px3, py3, px1, py1 SetColor 255,0,0 DrawRect p._x-10, p._y-25,20,5 SetColor 0,255,0 DrawRect p._x-10, p._y-25,20.0/PLAYER_HEALTH*p._health,5 End If i:+1 Next
For Local bt:TBullet = EachIn gamefield.bullets setColorForPlayer bt._orig DrawOval bt._x-2, bt._y-2, 4, 4 Next SetOrigin 0,0 End Method Method setColorForPlayer(p:TPlayer) For Local ptc:TPlayerToColor = EachIn colorMap If ptc.player=p SetColor ptc.r, ptc.g, ptc.b Return End If Next Local ptc:TPLayerToColor = New TPlayerToColor ptc.player = p ptc.r = colors[colorIDX*3] ptc.g = colors[colorIDX*3+1] ptc.b = colors[colorIDX*3+2] SetColor ptc.r, ptc.g, ptc.b colorMap.addLast(ptc) colorIDX = (colorIDX +1) Mod 6 End Method End Type
SampleKI.bmx
BlitzMax: [AUSKLAPPEN] [EINKLAPPEN] SuperStrict Import "GameBase.bmx" Type THumanPlayer Extends TPlayer Method update() If KeyDown(KEY_LEFT) turn(+4) If KeyDown(KEY_RIGHT) turn(-4) If KeyDown(KEY_UP) speedUp() Else If KeyDown(Key_DOWN) SpeedDown() Else break() End If If KeyDown(KEY_SPACE) shoot() End Method End Type
Type TSimpleKI Extends TPlayer Method update() Local enemy:TPlayer For Local p:TPLayer = EachIn getOtherPlayers() If p.isDead() Continue enemy = p Exit Next If enemy=Null break() Return End If
Local riToSet:Int = (ATan2(getY()-enemy.getY(), enemy.getX()-getX()) + 360) Mod 360 Local ldiff:Int = (riToSet-getRi() +360) Mod 360 Local rdiff:Int = (getRi()-riToSet +360) Mod 360 If(ldiff < rdiff) turn(ldiff) Else turn(-rdiff) End If Local ridiff:Int = Min(ldiff,rdiff) Local pdiff:Int = Sqr((getY()-enemy.gety())^2 + (getX()-enemy.getX())^2) If pdiff > getSpeed()*20 + 20 If(ridiff) = 0 speedUp() Else If (ridiff) < 50 If getSpeed() < ridiff/10 speedDown() Else speedUp() Else speedDown() End If Else break() End If If ridiff<=1 shoot() End Method End Type
Start.bmx
BlitzMax: [AUSKLAPPEN] [EINKLAPPEN] SuperStrict Import "GameBase.bmx" Import "SampleRenderer.bmx" Import "SampleKI.bmx" SeedRnd MilliSecs() Local gameField:TGameField = New TGameField Local renderer:TGameRenderer = New TSampleGameRenderer gameField.addPlayer(New THumanPlayer)
For Local x:Int = 1 To 3 Local p2:TPlayer = New TSimpleKI p2._x = Rand(0,550) p2._y = Rand(0,550) p2._ri = Rand(0,360) gamefield.addPlayer(p2) Next gamefield.setSize(550,550) renderer.gameField = gameField;
Graphics 800,600
Repeat If(AppTerminate()) End gameField.update() Cls renderer.draw(125,25) Flip Until KeyHit(KEY_ESCAPE)
Zip-Datei
Downloaden und alles in ein verzeichniss entpacken. die "start.bmx" sollte direkt lauffähig sein.
Ein paar Erklärungen:
in gameBase.bmx befindet sich alles, was für das eigentliche Framework wichtig ist:
- TGameField
Beschreibt das eigentliche Spielfeld
- TPlayer
Abstrakte klasse für einen Spieler. Diese Klasse wird für KI-Implementationen erweitert.
- TGameRenderer
Abstrakte klasse für den Renderer, also das Teil, was das Spielfeld zeichnet. Ich habe einen Simplen Beispielrenderer mitgegeben. Sollte sich wer berufen fühlen, kann er sich natürlich auch daran versuchen, da etwas schönes zu basteln
- TBullet
Klasse für einen abgegebenen Schuss
SampleRenderer.bmx enthält die oben erwähnte Beispielimplementation für den Renderer.
SampleKI.bmx enthält die oben erwähnte Beispielimplementation für eine einfache KI und auch die Player-Klasse für den Menschlichen Spieler (Steuerung per Tastatur)
Start.bmx setzt alle Teile zu einer lauffähigen Demo zusammen
Hinweise für eine Implementation: Da BlitzMax keine richtigen privates kennt, habe ich mich einfach auf eine eigene Konvention festgelegt: "Private" felder und methoden beginnen mit einem unterstrich. Solche dürfen in der implementation nicht verwendet werden. Alles andere ist erlaubt. (Ich hoffe, ich habe alles abgesichert, dass ein mogeln auf diese Weise nicht möglich sein sollte. Wenn doch, lasst es mich wissen)
Eine neue KI erstellt ihr, in dem ihr TPlayer erweitert und die update-methode überschreibt. Diese Methode stellt sozusagen einen einzelnen Spielzug dar.
Folgende Methoden könnt ihr in eurer KI verwenden:
turn(int), um sich zu drehen. Man kann der funktion sowohl positive und negative werte übergeben. Es gibt allerdings eine maximale Drehgeschwindigkeit, wenn diese überschritten wird, dreht sich die figur mit der maximal möglichen Geschwindigkeit in diese richtung
speedUp(), um die Geschwindigkeit zu erhöhen. Wenn die maximal mögliche Geschwindigkeit erreicht wird, passiert nichts
speedDown(), um die Geschwindigkeit zu drosseln. Die Figur kann sich mit einer langsamen Geschwindigkeit auch Rückwärts bewegen, was ebenfalls mit speedDown() erreicht wird.
break(), um zu bremsen. Das bedeutet einfach, dass, wenn die geschwindigkeit positiv ist, speedDown() aufgerufen wird, ansonsten speedUp(). Ausserdem stellt die funktion sicher, dass, wenn die geschwindigkeit nahe 0 ist, diese wirklich auf 0 gesetzt wird, anstatt in die andere Richtung zu gehen.
shoot(), um zu Schiessen. Je schneller man sich gerade bewegt, desto schneller fliegt auch die abgefeuerte Kugel.
getEnemys() liefert eine Liste mit allen Spielern auf dem Spielfeld, ausgenommen dem eigenen
getBullets() liefert eine Liste mit allen Kugeln, die gerade unterwegs sind.
Mein TODO:
- Skillpunkte-System. Es wird verschiedene Attribute geben (Geschwindigkeit, Wendigkeit, Beschleunigung, Kugelgeschwindigkeit, Kugelstärke und Energie fallen mir gerade Spontan ein), vor dem Spielbeginn wählt die KI die Verteilung ihrer Punkte.
- Teamspiel
So, das wars zur zeit. Für Fragen und Verbesserungsvorschläge bin ich natürlich offen.
Ich werd die Gelegenheit wohl nutzen und hier gleich mein erstes Worklog anlegen.
Grüße,
Smily
|