Flackern trotz Offscreen Image / Doublebuffer, (+ Frage zu Pixelvergleich)

Zul

Mitglied
Hallo zusammen, ich brauche Hilfe bei der Verwendung von Offscreen Images und habe einige Fragen dazu. Ich hoffe, ihr könnt mir helfen. 🙂


Momentan versuche ich ein kleines Spiel zu programmieren (so eine Art primitives 2D Rollenspiel, in dem man bis jetzt noch nichts machen kann).
Dazu habe ich ein 2dimensionales Array erstellt, das für die Darstellung der Welt dient, indem ich in jedem Tupel abspeicher, welches Bild dort gemalt werden soll.
Java:
private Tiles[][] world;
Dazu verwende ich BufferedImages, die ich im Konstruktor einlese. Meine Klasse, in der gezeichnet werden soll, ist ein JPanel. Ich habe das ganze nun zuerst ohne Offscreen Images realisiert und das Flackern hielt sich auch in Grenzen. Alles wird per repaint() in der Klasse, die ein Objekt dieses JPanels erstellt, alle 30ms neu gezeichnet.

Java:
public void paint(Graphics g) {
		super.paint(g);
		
		Graphics2D g2d = (Graphics2D)g;

		for (int j = game.getMapY1(); j < game.getMapY2() && j < world[0].length; j++) {
			for (int i = game.getMapX1(); i < game.getMapX2() && i < world.length; i++) {
				if(world[i][j].getTerrain()!=null){
					if (world[i][j].getTerrain() == "grass"){
						g.drawImage(grass, i * TILESIZE + game.getViewX(), j * TILESIZE
								+ game.getViewY(), null);
					}
				}
			}
		}

		//usw

}
(game.getMapY1() ist hier der obere Rand des sichtbaren Bereichs, game.getMapY2(), der untere. Dasselbe mit X für linken und rechten Rand. game.getView.. sorgt für das Verschieben des sichtbaren Ausschnitts der Welt.)

Dann kam mir die dumme Idee, Lichtquellen in der Welt zu benutzen und damit fing mein Ärger an und dort liegt auch mein Problem.

Da ich nämlich 1) statische Lichtquellen einbauen wollte, habe ich zusätzlich in jedem Tupel festgelegt, ob dort eine Lichtquelle sein soll oder nicht. Entsprechend bin ich nochmals das ganze Array durchgegangen und habe entweder ein halbtransparentes schwarzes Bildchen über das alte Bild gemalt oder ein schwarzes, beinahe vollkommen transparentes, damit das alte Bild darunter noch gut zu sehen ist.
So weit, so gut. Aber damit war ich leider nicht zufrieden. Da der Charakter nahezu ständig im Dunkeln tappte, 2) sollte es auch bei ihm hell sein. Deshalb habe ich ein komplett schwarzes Bild, was zur Mitte hin transparent wird über das gesamte Panel gezeichnet (der Charakter steht immer in der Mitte vom Panel).
Dabei habe ich jedoch nicht bedacht, dass ich die Dunkelheit mit 1) und 2) doppelt gemalt habe und auch die transparenten Stellen einmal übermalt werden.

Darum habe ich mir überlegt, dass ich die Dunkelheit mitsamt den transparenten Stellen aus 1) zuerst auf ein Offscreen Image zeichne und dann alle Pixel mit dem Bild aus 2) vergleiche, jeweils den helleren Pixel wähle und das neu entstandene Bild über das Panel zeichne. Und wo ich gerade eh dabei bin, ein Offscreen Image zu erstellen, hatte ich auch vor, das bisherige Bild zuerst auf so ein Offscreen Image zu zeichnen und danach erst auf das Panel.

--

An dieser Stelle muss ich sagen, dass mein Panel fast 600x400 groß ist und ihr spätestens hier schon aufhören könnt soviel Text zu lesen, wenn ihr denkt, dass das ganze viel zu viele Rechnungen benötigt, um den ganzen Vorgang ca. alle 30ms zu wiederholen. 🙁

--

Wenn es jedoch möglich ist, freue ich mich schonmal.🙂

Ich habe dann folgende Images und Graphics-Objekte erstellt:
Java:
private BufferedImage offScreen1;
private Graphics2D off1;
private BufferedImage offScreen2;
private Graphics2D off2;
im Konstruktor:
Java:
offScreen1= new BufferedImage(544,416,BufferedImage.TYPE_INT_ARGB);
off1= offScreen1.createGraphics();
offScreen2= new BufferedImage(544,416,BufferedImage.TYPE_INT_ARGB);
off2= offScreen2.createGraphics();
(komische Größe, ich weiß😉)

Alles was vorher mit 'g.draw' anfing, habe ich mit durch 'off1.draw' ersetzt, am Anfang der paint()-Methode rufe ich jetzt noch
Java:
off1.clearRect(0,0,544,416);
und am Ende
Java:
g.drawImage(offScreenImageDay,0,0,null);
auf.

1. Problem: Das funktioniert - aber leider nur beinahe. Denn gezeichnet wird zwar auf diese Art und Weise, jedoch kommt es ab und zu immer noch vor, dass das Bild flackert und ich weiß nicht, woran das liegt, denn eigentlich wird ja nur noch ein einziges Bild gezeichnet und nicht mehr viele kleine. Muss ich vielleicht die update-methode überschreiben? Irgendwo habe ich sowas mal gelesen. 🙁

2. Problem: Das andere Offscreen Image (offScreen2), dass ein halb transparentes Schwarzes Bild mit "transparenten Löchern" darstellt, kann ich nicht über das erste malen, da der Hintergrund des Graphics-Objektes nicht transparent ist. Gibt es dafür eine Möglichkeit?

3. Problem: Falls das alles mal klappen sollte, bin ich nicht ganz sicher, wie ich die einzelnen Pixel der Bilder vergleichen kann. ???:L

Am meisten stört mich momentan das Geflacker...
Ich hoffe, ich habe nicht zuviel geschrieben oder versuche etwas zu absurdes zu realisieren. Vielleicht könnt ihr mir ja helfen. 🙂


Lieben gruß, Zul
 
Hmm, okay.. Ich glaube, dass das Flackern verschwunden ist, nachdem ich statt der paint-Methode die paintComponent-Mehode überschrieben habe. 😳

Und das zweite Offscreen-Image scheint auch zu funktionieren, wenn ich vor
Java:
off2.clearRect(0,0,544,416);
Java:
off2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
aufrufe.
 
Zuletzt bearbeitet:
Ja, schon Pixel. Ich dachte, ich halte es schlicht und nehme nur 2-3 Bilder, die ich selbst erstellt habe, wo die Mitte transparent ist und es nach außen hin dunkler wird. So etwa:
h2qd995h.png

Die kann ich dann in ein Tile speichern und alle Tiles, die es überdeckt, werden auch als belegt gesetzt. Alle übrigen bekommen ein Bild welches nur dunkel ist. Damit habe ich ein Bild, welches die Umgebung abdunkelt und nur an festgelegten Stellen transparent ist und somit den Eindruck erweckt, dass es dort hell ist.
Da der Charakter aber wenigstens den Boden direkt unter sich sehen soll - auch im Dunkeln - möchte ich zusätzlich obiges Bild in groß über das gesamte Panel zeichnen.
Problem ist, dass ich die Dunkelheit dann doppelt eingefügt habe, darum möchte ich jedes Pixel von den beiden Bildern vergleichen und nur jeweils das hellere zeichnen.

Lese momentan gerade das verlinkte Thema. Danke. 😉
 
Ja so ein statisches Bild macht nur bedingt Sinn... Schau vielleicht doch mal das KSKB aus dem verlinkten Thread an, vielleicht ist das doch nicht so weit weg, von dem was du vorhast...
 
Jo, das gefällt mir sehr gut. Sieht auch viel eleganter aus als ein ständiger Pixelvergleich. Ich werde versuchen, das auf diese Weise zu lösen. Danke dir! 🙂
 
Huhu, also die Vorgehensweise aus deinem KSKB funktioniert gut. Wenn ich da noch herausfinde, ob ich den Übergang vielleicht noch ein klein wenig "unschärfer" hinbekomme und wie ich da noch noch andere Formen verwenden kann, wäre ich sehr zufrieden. 🙂

Es flackert übrigens wieder!? Ich weiß nicht, ob es vorher Glück war oder ob es wieder da ist, aber ich habe eigentlich nichts verändert.. hier ein Stück Wiese, wo manchmal eine andere Bodentextur darunter durchflackert.
w4y2tn9r.png

Edit: Und auch der schwarze Hintergrund kommt noch manchmal zum Vorschein.🙁
 
Zuletzt bearbeitet:
Jo, bitte schön. 🙂

Java:
public void paintComponent(Graphics g) {
		
		super.paintComponent(g);
		
		Graphics2D g2d = (Graphics2D)g;
		
		// 1. Offscreen
		day.clearRect(0,0,544,416);
		day.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, lightFactor));
		
		
		/*
		 * Gelaende
		 */
		for (int j = game.getMapY1(); j < game.getMapY2() && j < world[0].length; j++) {
			for (int i = game.getMapX1(); i < game.getMapX2() && i < world.length; i++) {
				if(world[i][j].getTerrain()!=null){
					if (world[i][j].getTerrain() == "grass"){
						day.drawImage(grass, i * TILESIZE + game.getViewX(), j * TILESIZE
								+ game.getViewY(), null);
					}else if (world[i][j].getTerrain() == "sand"){
						day.drawImage(sandOnly, i * TILESIZE + game.getViewX(), j * TILESIZE
								+ game.getViewY(), null);
					}
					// usw
				}
			}
		}

		/*
		 * Objekte und Umgebung
		 */
		for (int j = game.getMapY1(); j < player.getTilePositionY() && j < world[0].length; j++) {
			for (int i = game.getMapX1(); i < game.getMapX2() && i < world.length; i++) {
				
				if(world[i][j].getObject()!=null){
					day.drawImage(drawObject(i,j),i * TILESIZE + game.getViewX(), j * TILESIZE
								+ game.getViewY() - (drawObject(i, j).getHeight()-TILESIZE), null);
				}
				
				if(world[i][j].getEnvironment()!=null){
					day.drawImage(drawEnvironment(i,j),i * TILESIZE + game.getViewX(), j * TILESIZE
							+ game.getViewY() - (drawEnvironment(i, j).getHeight()-TILESIZE), null);
				}
			}
			// usw
		}

		// [...]

		// 2. Offscreen
		night.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
		night.clearRect(0, 0, 544,416);
		
		drawLights();

		// Zeichnen auf Panel
		g.drawImage(offScreenImageDay,0,0,null);
		g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, sunLight));
		g.drawImage(offScreenImageNight,0,0,null);
}

Java:
	private void drawLights(){
        night.setComposite(AlphaComposite.Src);
        night.setColor(new Color(0,0,16,240));
        night.fillRect(0,0,getWidth(),getHeight());
 
        for (int j = game.getMapY1(); j < game.getMapY2() && j < world[0].length; j++) {
			for (int i = game.getMapX1(); i < game.getMapX2() && i < world.length; i++) {
				if(world[i][j].getLight()){
					drawLight(night, new Point(i * TILESIZE + game.getViewX() + TILESIZE/2, j * TILESIZE
							+ game.getViewY() + TILESIZE/2),world[i][j].getLightRadius());
				}
			}
		}
        drawLight(night, characterPoint,75);
        
//      night.dispose();
    }
 
    private void drawLight(Graphics2D g, Point pt, int radius){
        g.setComposite(AlphaComposite.DstOut);
        Point2D center = new Point2D.Float(pt.x, pt.y);
        float[] dist = {0.0f, 1.0f};
        Color[] colors = {new Color(255,255,255,255), new Color(0,255,0,0) };
        RadialGradientPaint p = new RadialGradientPaint(center, (float)radius, dist, colors, CycleMethod.NO_CYCLE);
        g.setPaint(p);
        g.fillOval(pt.x-radius,pt.y-radius,radius*2,radius*2);
    }

An Stellen, wo nur Bodentextur gemalt wird, sieht man beim Flackern den schwarzen Panel-Hintergrund und an Stellen, wo zusätzlich noch etwas drübergezeichnet ist, sieht man die Bodentextur darunter. Also immer die darunterliegende Ebene. Ich wusste nicht, dass beim Offscreen noch die "Schichten" existieren vom mehrmaligem Zeichnen an derselben Stelle.

Die Bildfehler treten allerdings auch nur dann ab und zu auf, wenn man sich mit dem Charakter bewegt. Wenn ich nichts tue, habe ich jedenfalls noch keine feststellen können.
Bei der Bewegung starte ich einen Thread, der den Bildausschnitt alle 30ms um 2 Pixel verschiebt - repaint wird ebenfalls alle 30ms aufgerufen. Momentan wühle ich wegen Threads synchronisieren rum, falls es daran liegt. 🙁

Und danke nochmal für deine Geduld.
 
OK, das sieht recht kompliziert aus, aber beim Überfliegen sieht man zumindest nichts "falsches". Was aber kritisch sein könnte, ist, was du am Ende gesagt hast: Wenn ein anderer Thread "den Bildausschnitt verschiebt" (was auch immer das genau bedeutet) dann könnte es sein, dass der Klasse, die das ganze zeichnet, während des Zeichnens die Daten, die sie zeichnen soll, "unter dem Hintern weg" verschoben werden, und deswegen was falsches rauskommt.

NUR (NUR NUR NUR!) um das zu testen (und um es danach ggf. SOFORT wieder Rückgängig zu machen und ggf. anders zu lösen!) könnte man sowas machen wie
Java:
public void paintComponent(Graphics g) 
{
    synchronized(game)
    {
        doPaintComponentSync(g); 
    }
}

private void doPaintComponentSync(Graphics g)
{
    super.paintComponent(g);
    ...
    // Wie vorher...
}


// Dort wo die Bewgung durchgeführt wird
void move()
{
    // vorher:
    //doMovement(2,2);

   // Nachher
   synchronized(game)
   {
        doMovement(2,2);
   }
}

(Oh-ho... vielleicht sollte ich das nicht so vorschlagen...)
 
Jo, das funktioniert. Spitze! 🙂

Ähm, eine Frage noch. Kann ich den Übergang bei der "Taschenlampe" noch fließender machen? Ich habe bereits mit dem Gradient rumgespielt, allerdings nicht sonderlich erfolgreich.
 
Dann sollte man sich jetzt überlegen, wie man es RICHTIG machen könnte. paintComponent und irgendwas anderes in dieser Form zu synchronizen ist die Holzhammer-Methode...

Um den RadialGradientPaint "fließender" zu machen, müßtest du wohl
float[] dist = {0.0f, 1.0f};
Color[] colors = {new Color(255,255,255,255), new Color(0,255,0,0) };
in feinere Zwischenschritte Unterteilen, GROB sowas wie
float[] dist = {0.0f, 0.25f, 1.0f};
Color[] colors = {new Color(255,255,255,255), new Color(128,255,128,128), new Color(0,255,0,0) };
um der (in der Natur gegebenen) quadratischen Attenuation näher zu kommen
 
Das mit dem RadialGradientPaint werde ich austesten, danke. 🙂


Zu dem Synchronisieren: meine Move-Klasse erbt von Thread und wird aufgerufen, wenn man eine der Pfeiltasten drückt. Dann wird das Bild in 2-Pixel-Schritten in die entsprechende Richtung verschoben um die Bewegung zu simulieren. Also "Pfeil rechts", Bild wird nach links verschoben.

Sollte ich das ganze lieber mit aktivem Rendern lösen und das JPanel in einem eigenen Thread ständig neu zeichnen und dann das repaint aus der run-Methode vom Spiel rausnehmen? Wäre das besser, statt paintComponent zu synchronisieren?
 
Naja, man kann sich da tausende Lösungen überlegen, je nachdem, wie ausgefeilt das sein soll. Eine immernoch recht pragmatischen Lösung (von der man NICHT pauschal sagen kann ob sie hier angebracht wäre oder nicht!) wäre, die Änderungen, die sich auf zu zeichnende Dinge beziehen, mit SwingUtilities.invokeLater auf den EDT zu legen - sofern nicht wiederum ein anderer Thread auch noch diese Daten braucht...
 
Es ist immer schlau, die Dinge die man darstellen will, vor dem Darstellen zu kopieren und die Kopie darstellen, dann kann dir kein anderer Thread irgendwas unter dem Hintern wegändern und du sparst die Synchronisation.
 
Move-Klasse erbt von Thread und wird aufgerufen, wenn man eine der Pfeiltasten drückt.
Das finde ich eher etwas suboptimal. Wenn ich einmal alle 4 Richtungstasten drücke starte ich 4 Threads? Move ist sowieso kein Name für eine Klasse, eher für eine Funktion. Ich würde die Tastenabfrage einfach in der Mainloop machen.
 
Es ist immer schlau, die Dinge die man darstellen will, vor dem Darstellen zu kopieren und die Kopie darstellen, dann kann dir kein anderer Thread irgendwas unter dem Hintern wegändern und du sparst die Synchronisation.

Ja, so ist das: Pauschale Aussagen sind IMMER falsch 😀 Wenn das, was man visualisieren will, über "Spielzeug-Beispiele" hinausgeht, ist ein Kopieren pro Zeichenoperation nicht mehr tragbar. Ich habe da schon interessante Konstrukte gesehen...
Java:
void paintSuff(Collection<T> collection)
{
    try
    {
        for (T t : collection) paint(t);
    }
    catch (ConcurrentModificationException e)
    {
        paintStuff(collection); // Try again...
    }
}
Und auch wenn es mir (und wohl vielen anderen auch) bei sowas kalt den Rücken rauf und runter läuft: Es hat seine Rechtfertigung... (Trotzdem würde ich das so auch niemandem unbedacht empfehlen... 😉 )
 
Zu dem Synchronisieren: meine Move-Klasse erbt von Thread und wird aufgerufen, wenn man eine der Pfeiltasten drückt.
...
Sollte ich das ganze lieber mit aktivem Rendern lösen und das JPanel in einem eigenen Thread ständig neu zeichnen und dann das repaint aus der run-Methode vom Spiel rausnehmen? Wäre das besser, statt paintComponent zu synchronisieren?
Das "Synchronisationsproblem" ist mir nicht ganz klar. Was soll da synchronisiert werden? Im Prinzip gibt es ja nur die GameLoop in der Daten verändert werden. Wenn die Daten nicht (immer) korrekt dargestellt werden, sollte das doch kaum auffallen, da ohnehin mehrmals pro Sekunde neu gezeichnet wird.
Dann wird das Bild in 2-Pixel-Schritten in die entsprechende Richtung verschoben um die Bewegung zu simulieren. Also "Pfeil rechts", Bild wird nach links verschoben.
Habe mal Deine Spielidee mit meinem GameDemo Code kombiniert. Die Steuerung funktioniert mittels KeyBindings (Tasten A, W, D). Es werden hierbei "Flags" gesetzt, die in der Gameloop ausgewertet werden. Allerdings bewegt sich hier die Figur statt des Hintergrunds und das Rendering erfolgt passiv nur durch Überschreiben der paintComponent.
Java:
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;

import javax.swing.AbstractAction;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

@SuppressWarnings("serial")
public class TorchGame extends JPanel {
	private static int width = 600;
	private static int height = 600;
	private BufferedImage bgImage;
	private Color shadowColor = new Color(0, 0, 0, 200);
	private Color baseLight = new Color(255, 255, 0, 120);
	private Color playerColor = new Color(80, 0, 80);
	
	private Player player;
	private boolean move, left, right;

	private Candle[] candles;

	public TorchGame() {
		initEnvironment();
		setKeyBindings();
		this.setPreferredSize(new Dimension(width, height));
		
		//start game loop
		new Thread(new Runnable() {
			int shadowLevel = 200;
			int vec = 1;
			public void run() {
				while(true) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					calculateNextPos();
					// daytime loop
					shadowLevel += vec*2;
					if (shadowLevel<0) {
						shadowLevel=0;
						vec = 1;
					}
					else if	(shadowLevel>255) {
						shadowLevel=255;
						vec = -1;
					}
					setShadowLevel(shadowLevel);
				}
			}
		}).start();
	}
	
	//initialize game environment
	private void initEnvironment() {
		player = new Player(width/2, height/2);
		createBackground();
		candles = new Candle[width/50];
		for (int i = 0; i < candles.length; i++) {
			int dia = (int) (Math.random() * 150) + 50;
			int x = (int) (Math.random() * (width - dia - 10)) + 10;
			int y = (int) (Math.random() * (height - dia - 10)) + 10;
			candles[i] = new Candle(x, y, dia);
		}
	}
	
	// create random background image
	private void createBackground() {
		bgImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
		Graphics2D g = bgImage.createGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g.setColor(Color.LIGHT_GRAY);
		g.fillRect(0, 0, width, height);
		g.setStroke(new BasicStroke(5));
		for (int i = 0; i < width/40; i++) {
			int dia = (int) (Math.random() * 100) +10;
			int w = dia;
			int x = (int) (Math.random() * (width - dia - 10)) + 10;
			int y = (int) (Math.random() * (height - dia - 10)) + 10;
			if(dia%3==0) {
				g.setColor(Color.ORANGE);
				w *=.75;
			}
			else
				g.setColor(Color.BLUE);
			if (dia%2==0)
				g.drawRect(x, y, dia, w);
			else
				g.drawOval(x, y, dia, w);
		}
		g.dispose();
	}
	
	private void setKeyBindings() {
		InputMap map = this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		//player control move forward
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), "go");
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "stop");
		getActionMap().put("go", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				move = true;
			}
		});
		getActionMap().put("stop", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				move = false;
			}
		});
		//player control right
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), "right");
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "stopRight");
		getActionMap().put("right", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				right = true;
				left = false;
			}
		});
		getActionMap().put("stopRight", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				right = false;
			}
		});
		//player control left
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), "left");
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "stopLeft");
		getActionMap().put("left", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				left = true;
				right = false;
			}
		});
		getActionMap().put("stopLeft", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				left = false;
			}
		});
		
		// reset environment and player position
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "refresh");
		getActionMap().put("refresh", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				initEnvironment();
			}
		});
	}
	
	private void calculateNextPos(){
		if (right)
			player.turnRight();
		else if (left)
			player.turnLeft();
		if (move) {
			player.move(10);
			int x = player.getX();
			int y = player.getY();
			if(x<0 || y<0 || x>width || y >height)
				player.setBack(20);
		}
		repaint();
	}
	
	public void setShadowLevel(int level) {
		shadowColor = new Color(0, 0, 0, level);
		baseLight = new Color(255, 255, 0, 120*level/255);
		repaint();
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		g.drawImage(bgImage, 0, 0, null);
		Graphics2D g2 = (Graphics2D) g.create();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setColor(baseLight);
		g2.fillRect(0, 0, width, height);
		// painting candle bases
		g2.setColor(Color.RED);
		for (Candle candle : candles) {
			g2.fillOval(candle.getCenterX() - 5, candle.getCenterY() - 5, 10, 10);
		}
		
		// painting player
		g2.translate(player.getX(), player.getY());
		g2.rotate(Math.toRadians(player.getDir()));
		g2.setColor(shadowColor);
		g2.fillOval(-18, -15, 30, 30);
		g2.setColor(playerColor.brighter());
		g2.fillOval(-15, -15, 30, 30);
		g2.setColor(playerColor);
		g2.fillOval(-12, -12, 24, 24);
		g2.translate(5, 8);
		g2.fillRect(-5, -5, 25, 5);
		g2.dispose();

		BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
		g2 = image.createGraphics();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setColor(shadowColor);
		g2.fillRect(0, 0, width, height);

		// painting candle lights
		g2.setComposite(AlphaComposite.DstOut);
		for (Candle candle : candles) {
			g2.setPaint(candle.getPaint());
			g2.fillOval(candle.getX(), candle.getY(), candle.getDiameter(), candle.getDiameter());
		}

		// painting torch light
		g2.setComposite(AlphaComposite.DstOut);
		g2.translate(player.getX(), player.getY());
		g2.rotate(Math.toRadians(player.getDir()));
		g2.translate(35, 0);
		g2.setPaint(getTorchPaint(-10, 0, 50));
		g2.fillOval(-50, -40, 100, 80);

		g2.dispose();

		g.drawImage(image, 0, 0, null);
	}
	
	private Color[] torchColors = { new Color(0, 255, 0, 255), new Color(0, 255, 0, 255), new Color(0, 255, 0, 0) };

	private RadialGradientPaint getTorchPaint(int x, int y, int radius) {
		return new RadialGradientPaint(x, y, radius, new float[] { 0f, .5f, 1f }, torchColors);
	}
	
	class Player {
		private Point pos;
		private int dir;

		public Player(int x, int y) {
			pos = new Point(x, y);
			dir = 0;
		}

		public void move(int steps) {
			pos.x += Math.cos(Math.toRadians(dir)) * steps;
			pos.y += Math.sin(Math.toRadians(dir)) * steps;
		}
		
		public void setBack(int steps) {
			pos.x -= Math.cos(Math.toRadians(dir)) * steps;
			pos.y -= Math.sin(Math.toRadians(dir)) * steps;
		}

		public void turnRight() {
			dir = (dir + 10) % 360;
		}

		public void turnLeft() {
			dir = (dir - 10) % 360;
		}

		public int getX() {
			return pos.x;
		}

		public int getY() {
			return pos.y;
		}

		public int getDir() {
			return dir;
		}
	}
	
	class Candle {
		private Point position;
		private int dia;
		private Paint paint;
		private Color[] lightColors = { new Color(0, 0, 0, 220), new Color(0, 0, 0, 150), new Color(0, 0, 0, 220), new Color(0, 0, 0, 0) };

		public Candle(int x, int y, int dia) {
			this.position = new Point(x, y);
			this.dia = dia;
			this.paint = createPaint(x, y, dia / 2);
		}

		public int getX() {
			return position.x - dia / 2;
		}

		public int getY() {
			return position.y - dia / 2;
		}

		public int getCenterX() {
			return position.x;
		}

		public int getCenterY() {
			return position.y;
		}

		public int getDiameter() {
			return dia;
		}

		public Paint getPaint() {
			return paint;
		}

		private RadialGradientPaint createPaint(int centerX, int centerY, int radius) {
			return new RadialGradientPaint(centerX, centerY, radius, new float[] { 0f, 20f/dia, .7f, 1f }, lightColors);
		}
	}

	public static void main(String[] s) {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		final TorchGame panel = new TorchGame();
		frame.add(panel, BorderLayout.CENTER);
		frame.pack();
		frame.setLocationRelativeTo(null);
		frame.setVisible(true);
	}
}
 
@Michael: Habe mir gerade deine GameDemo angeguckt. Gefällt mir sehr gut. 🙂 Vor allem die Bewegung.


Ich hatte ja leider vorgesehen, dass keine diagonalen Bewegungen möglich sind. Man kann nur nach links, rechts, oben oder unten. Und immer wenn eine dieser Pfeiltasten gedrückt wird, die entsprechende Kachel begehbar ist und gerade keine Bewegung stattfindet, wird ein
Code:
boolean isMoving
auf true gesetzt, die Bewegung ausgeführt und sobald die Kachel erreicht ist, wieder auf false. So ist auch sichergestellt, dass immer nur ein Bewegungs-Thread gestartet wird. Wenn dieser abgeschlossen ist, wird direkt der nächste gestartet, falls noch eine Taste gedrückt (gehalten) wird. Die Steuerung funktioniert eigentlich ganz gut denke ich. 😳

Die Spielfigur befindet sich dabei immer in der Mitte des Bildschirms, sodass sich eigentlich nicht sie, sondern der Ausschnitt der Spielwelt sich verschiebt. Das mache ich mit
Code:
game.setViewX()
und
Code:
game.setViewY()
während des Bewegens. Und auf diese Werte greife ich halt auch beim Zeichnen (fast überall) zurück (s.o.). Das müssten dann die beiden Problemwerte sein, denke ich. Die Bildfehler prasseln ja auch gar nicht am laufenden Band auf den Bildschirm ein, aber auch das gelegentliche hat mich schon sehr gestört, wenn man es doch leicht vermeiden kann.

Naja, und mit dem synchronized funktioniert das momentan. Im JPanel:
Java:
synchronized(this){
	doPaintComponentSync(g); 
}
im Move-Thread an der Stelle wo der Bildschirm um x/y verschoben wird:
Java:
synchronized(game.getWorldPanel()){
	// [...]
	if (destinationX > currentX) {
				currentX += speed;
				player.setPositionX(currentX);
				game.setViewX(-speed);
			}else if (destinationX < currentX) {
				currentX -= speed;
				player.setPositionX(currentX);
				game.setViewX(speed);
			}else if (destinationY > currentY) {
				currentY += speed;
				player.setPositionY(currentY);
				game.setViewY(-speed);
			}else if (destinationY < currentY) {
				currentY -= speed;
				player.setPositionY(currentY);
				game.setViewY(speed);
			}
	// [...]
}

Hmm, wenn das mit dem 'synchronized' nicht so schön ist, muss ich mir mal Gedanken machen, wie man das sonst noch lösen kann...

@Marco13: Danke für den Tipp. Ich werde mich mal genauer zu SwingUtilities.invokeLater informieren. Ich kenne das nur ganz grob. 🙁

Ich freu mich, dass ihr versucht mir zu helfen. 🙂
Zul
 
im Move-Thread an der Stelle wo der Bildschirm um x/y verschoben wird:
Java:
synchronized(game.getWorldPanel()){
	// [...]
	if (destinationX > currentX) {
				currentX += speed;
				player.setPositionX(currentX);
				game.setViewX(-speed);
			}else if (destinationX < currentX) {
				currentX -= speed;
				player.setPositionX(currentX);
				game.setViewX(speed);
			}else if (destinationY > currentY) {
				currentY += speed;
				player.setPositionY(currentY);
				game.setViewY(-speed);
			}else if (destinationY < currentY) {
				currentY -= speed;
				player.setPositionY(currentY);
				game.setViewY(speed);
			}
	// [...]
}
Wenn ich das richtig verstanden habe, hast Du keine permanente GameLoop, sondern willst nur den Schritt von einer Position zur nächsten animiert darstellen!? Dann passt das schon für die Bewegung einen Thread zu starten. Sollte hier dann aber nicht irgendwie
Code:
while(destinationX > currentX)
o.ä. stehen.
Außerdem kann man ja anhand der Spielerposition und der Vorgabe, dass der Spieler in der Mitte des Fensters dargestellt werden soll, die Position des Spielfeldes berechnen und dieses entsprechend darstellen. Daher würde ich die Position des Spielfeldes nicht aktiv setzen --> weniger Daten zu synchronisieren.
 
Java:
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;

import javax.swing.AbstractAction;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;

@SuppressWarnings("serial")
public class TorchGame2 extends JPanel {
	private static int width = 400;
	private static int height = 400;
	private BufferedImage bgImage;
	private Color shadowColor = new Color(0, 0, 0, 200);
	private Color baseLight = new Color(255, 255, 0, 120);
	private Color playerColor = new Color(80, 0, 80);
	
	private Player player;
	private boolean move, left, right;

	private Candle[] candles;

	public TorchGame2() {
		initEnvironment();
		setKeyBindings();
		this.setPreferredSize(new Dimension(width, height));
		
		//start game loop
		new Thread(new Runnable() {
			int shadowLevel = 200;
			int vec = 1;
			public void run() {
				while(true) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					calculateNextPos();
					// daytime loop
					shadowLevel += vec*2;
					if (shadowLevel<0) {
						shadowLevel=0;
						vec = 1;
					}
					else if	(shadowLevel>255) {
						shadowLevel=255;
						vec = -1;
					}
					setShadowLevel(shadowLevel);
				}
			}
		}).start();
	}
	
	//initialize game environment
	private void initEnvironment() {
		player = new Player(width*3/2, height*3/2);
		createBackground();
		candles = new Candle[width*3/50];
		for (int i = 0; i < candles.length; i++) {
			int dia = (int) (Math.random() * 150) + 50;
			int x = (int) (Math.random() * (width*3 - dia - 10)) + 10;
			int y = (int) (Math.random() * (height*3 - dia - 10)) + 10;
			candles[i] = new Candle(x, y, dia);
		}
	}
	
	// create random background image
	private void createBackground() {
		bgImage = new BufferedImage(width*3, height*3, BufferedImage.TYPE_INT_RGB);
		Graphics2D g = bgImage.createGraphics();
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g.setColor(Color.LIGHT_GRAY);
		g.fillRect(0, 0, bgImage.getWidth(), bgImage.getHeight());
		g.setStroke(new BasicStroke(5));
		for (int i = 0; i < bgImage.getWidth()/40; i++) {
			int dia = (int) (Math.random() * 100) +10;
			int w = dia;
			int x = (int) (Math.random() * (bgImage.getWidth() - dia - 10)) + 10;
			int y = (int) (Math.random() * (bgImage.getHeight() - dia - 10)) + 10;
			if(dia%3==0) {
				g.setColor(Color.ORANGE);
				w *=.75;
			}
			else
				g.setColor(Color.BLUE);
			if (dia%2==0)
				g.drawRect(x, y, dia, w);
			else
				g.drawOval(x, y, dia, w);
		}
		g.setColor(Color.YELLOW);
		g.drawRect(0, 0, bgImage.getWidth(), bgImage.getHeight());
		g.dispose();
	}
	
	private void setKeyBindings() {
		InputMap map = this.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
		//player control move forward
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), "go");
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0, true), "stop");
		getActionMap().put("go", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				move = true;
			}
		});
		getActionMap().put("stop", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				move = false;
			}
		});
		//player control right
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0), "right");
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0, true), "stopRight");
		getActionMap().put("right", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				right = true;
				left = false;
			}
		});
		getActionMap().put("stopRight", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				right = false;
			}
		});
		//player control left
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0), "left");
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0, true), "stopLeft");
		getActionMap().put("left", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				left = true;
				right = false;
			}
		});
		getActionMap().put("stopLeft", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				left = false;
			}
		});
		
		// reset environment and player position
		map.put(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0), "refresh");
		getActionMap().put("refresh", new AbstractAction() {
			public void actionPerformed(ActionEvent evt) {
				initEnvironment();
			}
		});
	}
	
	private void calculateNextPos(){
		if (right)
			player.turnRight();
		else if (left)
			player.turnLeft();
		if (move) {
			player.move(10);
			int x = player.getX();
			int y = player.getY();
			if(x<0 || y<0 || x> bgImage.getWidth()|| y >bgImage.getHeight())
				player.setBack(20);
		}
		repaint();
	}
	
	public void setShadowLevel(int level) {
		shadowColor = new Color(0, 0, 0, level);
		baseLight = new Color(255, 255, 0, 120*level/255);
		repaint();
	}

	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		int pX = player.getX();
		int pY = player.getY();
		
		Graphics2D g2 = (Graphics2D) g.create();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.translate(width/2-pX, height/2-pY);
		g2.drawImage(bgImage, 0, 0, null);
		g2.dispose();
		
		g2 = (Graphics2D) g.create();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setColor(baseLight);
		g2.fillRect(0, 0, width, height);
		// painting candle bases
		g2.translate(width/2-pX, height/2-pY);
		g2.setColor(Color.RED);
		for (Candle candle : candles) {
			g2.fillOval(candle.getCenterX() - 5, candle.getCenterY() - 5, 10, 10);
		}
		
		// painting player
		g2.translate(player.getX(), player.getY());
		g2.rotate(Math.toRadians(player.getDir()));
		g2.setColor(shadowColor);
		g2.fillOval(-18, -15, 30, 30);
		g2.setColor(playerColor.brighter());
		g2.fillOval(-15, -15, 30, 30);
		g2.setColor(playerColor);
		g2.fillOval(-12, -12, 24, 24);
		g2.translate(5, 8);
		g2.fillRect(-5, -5, 25, 5);
		g2.dispose();

		BufferedImage image = new BufferedImage(bgImage.getWidth(), bgImage.getHeight(), BufferedImage.TYPE_INT_ARGB);
		g2 = image.createGraphics();
		g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		g2.setColor(shadowColor);
		g2.fillRect(0, 0, image.getWidth(), image.getHeight());

		// painting candle lights
		g2.setComposite(AlphaComposite.DstOut);
		for (Candle candle : candles) {
			g2.setPaint(candle.getPaint());
			g2.fillOval(candle.getX(), candle.getY(), candle.getDiameter(), candle.getDiameter());
		}

		// painting torch light
		g2.setComposite(AlphaComposite.DstOut);
		g2.translate(player.getX(), player.getY());
		g2.rotate(Math.toRadians(player.getDir()));
		g2.translate(35, 0);
		g2.setPaint(getTorchPaint(-10, 0, 50));
		g2.fillOval(-50, -40, 100, 80);

		g2.dispose();
		
		g2 = (Graphics2D) g.create();
		g2.translate(width/2-pX, height/2-pY);
		g2.drawImage(image, 0, 0, null);
		g2.dispose();
	}
	
	private Color[] torchColors = { new Color(0, 255, 0, 255), new Color(0, 255, 0, 255), new Color(0, 255, 0, 0) };

	private RadialGradientPaint getTorchPaint(int x, int y, int radius) {
		return new RadialGradientPaint(x, y, radius, new float[] { 0f, .5f, 1f }, torchColors);
	}
	
	class Player {
		private Point pos;
		private int dir;

		public Player(int x, int y) {
			pos = new Point(x, y);
			dir = 0;
		}

		public void move(int steps) {
			pos.x += Math.cos(Math.toRadians(dir)) * steps;
			pos.y += Math.sin(Math.toRadians(dir)) * steps;
		}
		
		public void setBack(int steps) {
			pos.x -= Math.cos(Math.toRadians(dir)) * steps;
			pos.y -= Math.sin(Math.toRadians(dir)) * steps;
		}

		public void turnRight() {
			dir = (dir + 10) % 360;
		}

		public void turnLeft() {
			dir = (dir - 10) % 360;
		}

		public int getX() {
			return pos.x;
		}

		public int getY() {
			return pos.y;
		}

		public int getDir() {
			return dir;
		}
	}
	
	class Candle {
		private Point position;
		private int dia;
		private Paint paint;
		private Color[] lightColors = { new Color(0, 0, 0, 220), new Color(0, 0, 0, 150), new Color(0, 0, 0, 220), new Color(0, 0, 0, 0) };

		public Candle(int x, int y, int dia) {
			this.position = new Point(x, y);
			this.dia = dia;
			this.paint = createPaint(x, y, dia / 2);
		}

		public int getX() {
			return position.x - dia / 2;
		}

		public int getY() {
			return position.y - dia / 2;
		}

		public int getCenterX() {
			return position.x;
		}

		public int getCenterY() {
			return position.y;
		}

		public int getDiameter() {
			return dia;
		}

		public Paint getPaint() {
			return paint;
		}

		private RadialGradientPaint createPaint(int centerX, int centerY, int radius) {
			return new RadialGradientPaint(centerX, centerY, radius, new float[] { 0f, 20f/dia, .7f, 1f }, lightColors);
		}
	}

	public static void main(String[] s) {
		JFrame frame = new JFrame();
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		final TorchGame2 panel = new TorchGame2();
		frame.add(panel, BorderLayout.CENTER);
		frame.pack();
		frame.setLocationRelativeTo(null);
		frame.setVisible(true);
	}
}
 
Jo, hab
Code:
while (destinationX != currentX || destinationY != currentY);
als Abbruchbedingung.

Hmm, bin ja noch nicht so fortgeschritten mit Java, deswegen habt bitte Nachsicht. 😉
Hier wird doch festgelegt, wo der gesamte Hintergrund gezeichnet wird, oder?
Java:
g2.translate(width/2-pX, height/2-pY);
Java:
int pX = player.getX();
int pY = player.getY();

Und das wird bei move == true gesetzt. Anders habe ich es ja eigentlich auch nicht gemacht (glaube ich), nur dass ich einen neuen Thread starte und deswegen manchmal gezeichnet wird, während x und y neu gesetzt wird. Oder habe ich etwas nicht verstanden? ???:L
 
Hier wird doch festgelegt, wo der gesamte Hintergrund gezeichnet wird, oder?
Java:
g2.translate(width/2-pX, height/2-pY);
Ja hier wird das Graphics Objekt des Panels so verschoben, dass z.B. das Hintergrundbild lagerichtig zur Figur gezeichnet wird.
Und das wird bei move == true gesetzt. Anders habe ich es ja eigentlich auch nicht gemacht (glaube ich), nur dass ich einen neuen Thread starte und deswegen manchmal gezeichnet wird, während x und y neu gesetzt wird.
Bei move==true wird nichts gesetzt. In meinem Bsp. wird grundsätzlich alle 100ms neu gezeichnet. In deinem Fall soll - wenn ich Dein Konzept richtig verstanden habe - nur bei der Bewegung von einer Kachel zur nächsten animiert werden. Das heißt während der Bewegung darf sich die Zielposition (steckt die in x und y?) eigentlich gar nicht verändert werden. Wenn es aber zulässig ist, dass der Anwender bereits in der Bewegung zur nächsten Kachel die nächste Zielkachel "anvisieren" kann, dann müsste man die Zielpostition in einem (synchronisierten) Puffer speichern.
 
Wenn man 'W' drückt, wird doch move = true gesetzt und wenn das der Fall ist, wird die move-Methode von Player aufgerufen, in der die Koordinaten des Punktes des Spielers verändert werden, die wiederum als Variablen zum Ausrichten des Graphics Objekts dienen.

In meinem Projekt wird (und soll) ebenfalls regelmäßig gezeichnet werden - unabhängig davon, ob der Spieler sich bewegt oder nicht. Jedoch lager ich die Bewegungsberechnung in einem Thread aus, in dem ich sagen wir mal pX und pY festlege. Wenn dann das Bild allerdings gerade in dem Augenblick gezeichnet wird, wo einer der Werte neu berechnet wird flackert das Bild kurz auf.

Naja... vielleicht habe ich das ganze auch einfach ganz dumm programmiert... 🙁
Mit dem synchronized funktioniert es zumindestens.
 
In meinem Projekt wird (und soll) ebenfalls regelmäßig gezeichnet werden - unabhängig davon, ob der Spieler sich bewegt oder nicht. Jedoch lager ich die Bewegungsberechnung in einem Thread aus, in dem ich sagen wir mal pX und pY festlege.
Warum wird die Bewegungsberechnung in einen eigenen Thread ausgelagert? Warum wird das nicht in dem Thread/ der GameLoop gemacht die das Neuzeichnen anfordert?
 
Ich dachte, ich stopfe die run-Methode nicht ganz so voll, wenn die Bewegungsberechnung auch in einem eigenen Thread abgewickelt werden kann. >300 loc woanders unterzubringen erschien mir sinnvoll um den Überblick zu behalten.


Das Thema hat sich erstmal erledigt denke ich. Habe momentan auch nicht die Zeit dafür. Nochmal danke für die Lösung für das Licht, Marco13. 😉
 

Zurück
Oben