Blitzbasic Tutorial
Robert "bob" Chlewinski Jan.2006

Bitmap to Rectangle Konverter.
Schwierigkeitsgrad: Ambitoniert Anfänger
Ziel: Ein Zeitsparendes Tool bei der Entwicklung von Buttons oder zu überwachender Bereiche auf dem Screen.

Komplett Download

Vorwort:
Hallo BlitzForum.de User. Ich möchte mich an dieser Stelle mal bei dem Forum für die gute Unterstützung bei meinen Projekten mit einem kleinen Anfänger Tutorial bedanken. In diesem Tutorial geht es um das Erstellen von Maussensitiven Breichen für Blitzbasic Anwendungen. Unter einem Maussensitiven Bereich verstehe ich eine beliebige Stelle auf dem Bildschirm die beim überfahren (mousemove) oder klicken (click) mit der Maus ein Ereignis auslösen kann. Dies müssen nicht immer nur Schalflächen (Buttons) sein. Oftmals werden, insbesondere bei Spielen, einige Bereiche des Bildschirms (meistens am Rand) dazu verwendet um z.B. eine Map zu scrollen oder andere Grafiken aufzunehmen. Zunächst ein bisschen aus dem Nähkästchen. Für ein aktuelles Blitzbasic Projekt (ein Rollenspiel) erstelle ich z.Zt. mittels Photoshop einige Screens. So z.B. die Inventarverwaltung der Charaktere. Wer schon mal ein Rollenspiel gespielt hat weis wie so etwas aussehen kann. Für diejenigen die mit diesem Spielgenre nicht so vertraut sind: Es gibt eine Menge "Ablageflächen" die Körperteile wie Kopf, Brust, Hände und vieles mehr repräsentieren. Bestimmte Gegenstände, z.B ein Schwert können nun dem Charakter in die Hand gegeben werden. Ein gefundener Helm kommt auf den Kopf usw. Für ein derartiges Ablagesystem müssen sehr viele Bereiche des Bildschirms dahin gehend überprüft werden ob die Maus grade über ihnen ist oder ob mittels Click ein Gegenstand abgelegt oder entfernt werden soll. Dies alles zu programmieren ist nicht nur sehr aufwendig es ist auch sehr langweilig. Das Nachfolgende Tutorial soll nun dazu beitragen diesen Aufwand drastisch zu reduzieren. Wir fangen mal mit dem Bild eines solchen Inventarsystems an.

Inventarsystem Final:


Problemstellung:
Die großen grauen Bereiche sollen im Spätern Spiel Detailinformationen über das aktuellen Item sowie einen Itembrowser und die aktuellen Werte (stats) des Charakters zeigen. Auch sie sind Bereiche die Teilweise auf Mausereignisse reagieren sollen. Davon sind aber statisch ihre exakte Position ist dennoch wichtig den dies Position wird dazu verwendet um andere Grafiken in sie hineine zu Zeichnen. Die kleinen Rechtecke im oberen rechten Bereich sind die Ablageplätze für Items die der Charakter momentan am Körper trägt oder besondere Verwendung finden.

Das Problem was sich ergibt, sobald man die Grafik in Blitzbasic geladen hat dürfte klar sein. Wie überwache ich alle diese Bereiche. Für dieses Problem gibt's eine ganze Hand voll Lösungsansätzen. Einige Programmierer würden einfach die grauen Flächen mittels Maskimage Befehl auf Transparent setzen und anschließend mit einer der Kollisionskontrollen für Transparente Bereiche …. Aber moment mal. Das geht ja gar nicht. Werden die Bereiche als Transparent definiert findet eben keine Kollision statt. Nun. Dann wird halt der schwarze Bereich Transparent gemacht und… Stop. Vergesst es. Wenn schwarz Transparent wird sind auch einige andere Bereich im Bild Transparent. So in etwa erging es mir. Der einfachste weg, so dachte ich mir zum Schluss: Definiere für jeden der Bereich ein Rechteck. Dann die Kollision mittels "RECTSOVERLAP" überprüfen. Fertig. Für die Suche nach einem geeignetem Tool habe ich leider, vergeblich viel Stunden vergeudet. Photoshop selbst, das über ein Info Fenster mit Koordinaten Ausgabe verfügt eignet sich nur schlecht für diese Aufgabe. Selektiert man nämlich in Photoshop inen Bereich bekommt man zwar kurzzeitig auch den Startpunkt (x,y) der Selektion angezeigt, dieser Verschwindet aber sobald man die Maustaste loslässt. Man muss dann, um den Startpunkt als Koordinaten zu sehen, die Maus erneut, manuell exakt auf die Position setzen und sich die Koordinaten notieren. Tut euch das nicht an. Ich zumindest habe mir überlegt wie viel Screes ich noch zu Zeichnen habe und wie viele Rechtecke das sind. Von kommenden Projekten ganz zu schweigen. Wen es kein Tool gib, dann uss man sich halt selbst eins machen.

Anmerkung:
Alle Grafiken liegen sowohl im Jpg als auch im BMP Dateie format vor: Die JPG Dateien dienen zur Anzeige dieses Tutorials über einen Browser. Hier der gesamte Quellcode für das Tool.

AppTitle "Bitmap to Rectangle Konverter by bob Jan. 2006"
Const ScreenX =1024
Const ScreenY =768
Const ScreenC =16
Const ScreenM =2
Graphics ScreenX,ScreenY,ScreenC,ScreenM
mask = loadimage("inventar_mask.bmp")
final= loadimage("inventar.bmp")

Type rec

Field x,y,b,h

End Type

Finde_Bereiche mask

SetBuffer BackBuffer()
While Not KeyHit(1)

Color 255,0,0

MX =Mousex()

MY = MouseY()

DrawBlock final,0,0

bCount =0

For r.rec = Each rec

bCount = bcount+1

If RECTSOVERLAP (mx, my, 1,1, r\x, r\y,r\b,r\h) Then

Rect r\x, r\y, r\b, r\h, 1

Color 255,255,255

If mousedown(1) then

Text r\x+5 ,r\y+3,"CLICK!"

Else

Color 255,0,0

Rect r\x, r\y, r\b, r\h, 0

Text r\x+5 ,r\y+3,bCount

EndIf

EndIf

Next

Flip

Wend
End

Function Finde_Bereiche(img)

SetBuffer ImageBuffer(img)

LockBuffer ImageBuffer(img)

endeX = ImageWidth(img)

endeY = Imageheight(img)

For y = 0 To endeY-1

For x = 0 To endeX

rgb = ReadPixelFast(x,y) And $FFFFFF

If rgb <> 0 And FoundStart=False Then

DebugLog rgb

r.rec = New rec

r\x = x

r\y = y

FoundStart = True

DebugLog "Found x,y at " + x +"|"+y

EndIf

If Rgb = 0 And Foundstart = True Then ;Horizontales End gefunden

r\b = x - r\x

Goto Weiter

EndIf

Next

Next

If foundstart = False Then

Return

EndIf

.weiter

x=x-1

For y = r\y To endeY

rgb = ReadPixelFast(x,y) And $FFFFFF

If rgb = 0 Then

r\h = y-r\y

Exit

EndIf

Next

If r\h =0 then r\h = y-r\y

UnlockBuffer ImageBuffer(img)

Color 0,0,0

Rect r\x,r\y,r\b,r\h

Finde_Bereiche img

End Function
Funktionsweise:
Anhand eines Schwarz weis Bitmaps (1 Bit Farbtiefe, also rein schwarz weis) werden alle weisen Stellen der Grafik identifiziert und in Rectangle Typen abgelegt. Das Programm arbeitet dabei sämtliche Pixel durch. Da dieses Programm ein Tool ist, und nicht Teil irgendeiner Engine werden soll, ist der Zeitaufwand den das Programm dabei benötigt unerheblich. Ich war dann zum Schluss dennoch überrascht dass es so schnell ging.

Erläuterungen zum Code.
Const ScreenX =1024
Const ScreenY =768
Const ScreenC =16
Const ScreenM =2
Graphics ScreenX,ScreenY,ScreenC,ScreenM
mask = loadimage("inventar_mask.bmp")
final= loadimage("inventar.bmp")
kopie = CopyImage(mask)
Type rec

Field x,y,b,h

End Type
Es werden Konstante für Die Auflösung , Farbtiefe und Windows Mode Festgelegt. Danach wird der Screen mittels Graphics Initialisiert und 2 Bitmaps geladen. Einmal das Fertige Bild (final) (Siehe oben) und einmal eine rein schwarz weis Entsprechung (mask). Das bedarf wohl keiner weiteren Erklärung. Der Type "rec" dient zur Aufnahme der Eigenschaften eines Rechtecks. Die Startkoordinate X,Y die der oberen linken Ecke eines Rechtecks Entsprechen sowie die breite (B) und höhe (H) des Rechtecks. Verwechselt Angaben B und H nicht mit Koordinaten. Es sind nämlich keine. Es folgt der Aufruf der Funktion die das Kernstück ist.

Finde_Bereiche mask

Hier wird als Parameter der Funktion das Image "mask" übergeben. (Habe das Image so genant weil es mich an alte Projekte erinnerte wo Schwarzweisbilder dazu dienten einen bestimmten Bereich einer Grafik als Transparent zu deklarieren.)

Function Finde_Bereiche(img)

SetBuffer ImageBuffer(img)

LockBuffer ImageBuffer(img)

endeX = ImageWidth(img)

endeY = Imageheight(img)

Die Funktion verlangt als einzigen Parameter ein geladenes Bild. Der Imagebuffer des Bildes wird anschließend gesperrt und breite und höhe des Bildes ermittelt.

For y = 0 To endeY-1

For x = 0 To endeX

rgb = ReadPixelFast(x,y) AND $FFFFFF

Es wird eine Doppelschleife (for Y, for X) eingeleitet. Wir stellen fest das die Schleifen sich über die gesamte Breite * Höhe des Bittmaps erstreckt. Bei der Auflösung des Bildes (736 * 736) sind dies immerhin 541696 Durchläufe. Die Variable rgb nimmt den Farbwert der Function ReadPixelFast auf. X und Y sind, na klar, die aktuelle x,y, position der Schleife die zusätzliche Angabe "And $FFFFFF" sorgt dafür das in der Variable rgb eine "Anständige" Zahl gespeichert wird (0 für schwarz und 16777215 für weis).

If rgb <> 0 And FoundStart=False Then

r.rec = New rec

r\x = x

r\y = y

FoundStart = True

'DebugLog "Found x,y at " + x +"|"+y

EndIf


Eine erste Entscheidung.
Hier wird überprüft ob auf der Bitmap an aktueller Schleifenposition (x,y) eine andere Farbe als schwarz gefunden wurde UND ob die Variable FoundStart noch den Wert "False" besitzt. (Die Variable wird gleichzeitig mit ihrem ersten auftauschen erstellt und ist somit, da sie keinen Wert hat, in jedem Fall = False.) Das heißt. Sobald das erste weise Pixel auftaucht, wird ein erster Type rec erstellt. Die aktuellen Koordinaten werden in den eigenschaften des Typs abgelegt. Foundstart wird nun auf True gesetzt somit kann diese If Abfrage in keinem Fall mehr erneut ausgeführt werden.

If rgb = 0 And Foundstart = True Then

;Horizontales End gefunden

r\b = x - r\x

Goto Weiter

EndIf

Next

Next


Eine zweite If Bedingung überprüft nun im weitern Verlauf der Doppelschleife ob ein schwarzes Pixel gefunden wurde UND ob FoundStart=True ist. Das heißt dass diese Bedingung nur dann erfüllt sein kann wen die vorherige Bedingung irgendwann mal erfüllt wurde. Gehen wir mal davon aus dass zuvor ein Weises Pixel gefunden wurde. (Foundstart ist dann = True) . Da wir wissen das wir die Farbe der Pixel erst auf Horizontaler Achse überprüfen, am ende der Bitmap ein Zeile tiefer Springen und dann wieder in horizontaler Richtung prüfen (Schreibmaschinen mässig), haben wir nun eine weise grade Linie abgearbeitet und sind nun an deren Ende auf ein schwarzes Pixel gestoßen. Hier haben wir dan den rechten Rand eines Bereichs gefunden. Wir notieren als die feststehend Breite. r\b =x -r\x (Breite = aktuelle Position - der bereits gefundenen Startposition des Rechtecks. Wir erinnern uns B ist KEINE Koordinate.) Sobald wir diesen Punkt erreicht haben können wir die Doppelschleife komplett verlassen. Wir springen einfach ab mittels "Goto Weiter".

.weiter

If foundstart = False Then Return
.weiter


Die erneute Abfrage ob foundstart = False ist dient dazu die Funktion an dieser Stelle mittels Return zu verlassen. Dies wäre z.B. dann der Fall wen die gesamte Bitmap schwarz wäre. Da die Sprungmark ".weiter" allerdings hinter der If abfrage liegt, wird diese nicht ausgeführt (Wenn wir davon ausgehen das wir vorher schon etwas "weises" gefunden haben.

Neue Schleife neues Glück.

X = x-1

For y = r\y To endeY

rgb = ReadPixelFast(x,y) AND $FFFFFF

If rgb = 0 Then

r\h = y-r\y

Exit

EndIf

Next
If r\h =0 then r\h = y-r\y


Wir erinnern uns. Wir haben für das erste zu erstellende Rechteck Bereits die X,Y Koordinaten sowie die Breite herausgefunden. Um die Angaben mit der Höhe des Rechtecks zu komplettieren brauchen wir nur eine Schleife. Sie beginnt bei der aktuellen y Position und endet, wenn nicht vorher die Höhe gefunden wurde, am unteren Rand der Bitmap. Vorsicht aber. Wir haben die Doppelschleife auf einem schwarzen Pixel verlassen. Wir müssen erstmal zurück in den weisen Bereich. Deshalb: x=x-1. Wir erfassen nun mit ReadPixelfast erneut die Farbe. Sobald diese wieder schwarz ist, haben wir auch die Höhe des Rechtecks ermittelt. "r\h = y-r\y". Wir verlassen nun mit "Exit" auch diese Schleife. Zur Vorsicht überprüfen wir noch mal ob auch wirklich etwas als Breite eingetragen wurde. Dies Könnte notwendig sein wen der weise Bereich bis zur letzten Y Position geht. Ein schwarzer Pixel würde dann nicht gefunden und die Breite wäre 0. Das wäre nicht so Toll. (Man könnte beim Finden eines schwarzen Pixels auch nur die Schleife einfach nur verlassen und die Breite anhand des aktuellen Stands von Y ermitteln. Aber was soll's.)

Das Ende Naht. Oder?

UnlockBuffer ImageBuffer(img)

Color 0,0,0

Rect r\x,r\y,r\b,r\h

Finde_Bereiche img
End Function


Wir sind kurz vor dem Ende der Funktion. Einige mögen sich schon gedanken gemacht haben was den mit den anderen Rectangels passiert. Einige merken es aber auch erst jetzt, da ich es erwähne. Jetzt kommt der Trick bei der ganzen Sache. Wir hebn mittels UnlockBuffer erstmal wieder die Sperrung des "Mask" Bitmaps auf um mit normalen Zeichenfunktionen auf dem Buffer zu malen. Da wir ja schon unser erstes Rectangle erfolgreich gefunden haben, testen wir es direkt mal aus. Rect r\x, r\y ,r\br r\h. Wichtig ist das wir unbedingt die aktuelle Farbe für Zeichnungsaktionen auf schwarz setzen. Color 0,0,0. Somit haben wir grade unser kompletten weisen ersten Bereich mit schwarz übermalt. Anschließend rufen wir die Funktion einfach erneut auf. (Rekursion. U.a. eine Funktion die sich selbst aufruft.) Das ganze Spiel beginnt somit von neuem. Allerdings….. Wir wissen ja nun das ein weiser Bereich nicht erneut gefunden werden kann. Wir haben ihn grade komplett übermalt. D.h. Früher oder später ist das ganze Image schwarz. Dann tritt folgendes ein.

If foundstart = False Then Return
.weiter

Erinnert Ihr euch? Foundstart liefert bei komplett schwarzen Bildern allein schon deshalb weil die Variable dann nie erstellt wird. Ist die Funktion nun endlich mittels Return verlassen, brauchen wir nur noch das Ergebnis unserer Bemühungen anzuzeigen. Auf eine großartige Kommentierung verzichte ich hier weil jeder der soweit durchgehalten hat dies locker selber enträtseln kann.
SetBuffer BackBuffer()
While Not KeyHit(1)

Color 255,0,0

MX =Mousex()

MY = MouseY()

DrawBlock final,0,0

bCount =0

For r.rec = Each rec

bCount = bcount+1

If RECTSOVERLAP (mx, my, 1,1, r\x, r\y,r\b,r\h) Then

Rect r\x, r\y, r\b, r\h, 1

Color 255,255,255

If mousedown(1) then

Text r\x+5 ,r\y+3,"CLICK!"

Else

Color 255,0,0

Rect r\x, r\y, r\b, r\h, 0

Text r\x+5 ,r\y+3,bCount

EndIf

EndIf

Next

Flip

Wend
End


Eine simple Endlosschleife wie sie in den meisten aller bb Programme zu finden ist. Wir zeigen die Originalgrafik und legen blenden zu Testzwecken die Rectangles ein.

Anmerkungen:
Der "Trick" einfach das zuletzt erstellte Rechteck dazu zu verwenden sein weises "Elternteil" einfach zu killen und die Funktion wieder mit der Doppelschleife ab 0,0 zu initialisieren, mag dem ein oder andern Programmiere als unsauber erscheinen. Es gibt eine reihe Ansätze Zeit bei der Funktion zu sparen. Zeit die mir allerdings dann für andere Projekte fehlen würde und die ich dazu verendete diese Tutoral zu schreiben. Seid also bitte nicht zu streng mit mir. Ich würde mich im übrigens sehr darüber freuen wenn jemand aus der "GUI Liga" für diese kleine Tool eine Benutzerfreundlich, kleine Oberfläche erstellen könnt. Eine Dateiauswahl für Bitmaps wäre sehr schön. Weiterhin nützt das ganze Programm natürlich auch nicht viel wen die erzeugten Rectangle Typen nicht in eine Datei gespeichert werden. Sie sollen ja später in einem ganz anderen Programm Verwendung finden. Der Type Rec könnte auch noch ein Zusätzliches Field gebrauchen um z.B. ein feste Verbindung zu der Bitmap zu gewährleisten.

Danke und viel Spaß beim tüfteln.

rpg@iomagic.de