Why can't I erase my Sprite

那年仲夏 提交于 2019-12-25 09:26:17

问题


I've been trying for a while now to get a game working in Java, after much anguish over other peoples Sprite functions I made my own and don't understand why I can't erase it. I know it's changing the pixels of the background to display my archer sprite since it shows up but for whatever reason I can't change the pixels back to what they were before. Does anyone have an idea why that is or how I can fix it? Link to google doc with images: https://docs.google.com/document/d/1eU6faW1d7valq1yE_Bo09IPMbXuuZ6ZgqUu3BesaJUw/edit?usp=sharing

import javax.swing.*;
import javax.imageio.*;
import java.io.*;
import java.awt.image.BufferedImage;

public class Sprite {
BufferedImage image;
public Sprite(BufferedImage image) throws IOException{
this.image = image;
}
public BufferedImage getSprite(){
return this.image;
}
public int getX(){
return this.image.getMinX();
}
public int getY(){
return this.image.getMinY();
}

//to spawn a sprite on top of another image.
public void spawn(JFrame frame, BufferedImage world,int x, int y) throws 
IOException{
int orig_x = x;
for (int sprite_y = 0; sprite_y < this.image.getHeight(); sprite_y++){
  for (int sprite_x = 0; sprite_x < this.image.getWidth(); sprite_x++){
    int sprite_pixel = this.image.getRGB(sprite_x,sprite_y);
    int sprite_alpha = (sprite_pixel>>24) & 0xff;
    int sprite_red   = (sprite_pixel>>16) & 0xff;
    int sprite_green = (sprite_pixel>>8 ) & 0xff;
    int sprite_blue  =  sprite_pixel      & 0xff;
    int pixel = (sprite_alpha<<24) | (sprite_red<<16) | (sprite_green<<8) | 
    sprite_blue;
    world.setRGB(x,y,pixel);
    x++;
    }
    y++;
    x = orig_x;
    }
    }

    public void erase(JFrame frame,BufferedImage world, BufferedImage 
    orig_world) throws IOException{
    int sprite_x = this.image.getMinX();
    int sprite_y = this.image.getMinY();
    int orig_sprite_x = sprite_x;
    for (int stepper_y = this.image.getMinY(); stepper_y < 
    this.image.getHeight(); stepper_y++){
      for (int stepper_x = this.image.getMinX(); stepper_x < 
      this.image.getWidth(); stepper_x++){
         int sprite_pixel =  orig_world.getRGB(sprite_x,sprite_y);
         //get pixel from orginal sprite
         int sprite_alpha = (sprite_pixel>>24) & 0xff;
         //get alpha value from original sprite
         int sprite_red   = (sprite_pixel>>16) & 0xff;
         //get red   value from original sprite
         int sprite_green = (sprite_pixel>>8 ) & 0xff;
         //get green value from original sprite
         int sprite_blue  =  sprite_pixel      & 0xff;
         //get blue  value from original sprite

         int pixel = (sprite_alpha<<24) | (sprite_red<<16) | 
         (sprite_green<<8) | sprite_blue;
         //set the pixel equal to the old values
         world.setRGB(sprite_x,sprite_y,pixel);
         //place the pixel
         sprite_x++;
         }
    sprite_x = orig_sprite_x;
    // setting equal to original is so that at the end of each row it resets 
    to the farthest left pixel.
    sprite_y++;
   }
 }

 public static void main(String[] args) throws IOException{

  Sprite orig_world = new Sprite(ImageIO.read(new 
  File("C:/Users/sfiel42/Documents/game/castles.png")));
  Sprite world      = new Sprite(ImageIO.read(new 
  File("C:/Users/sfiel42/Documents/game/castles.png")));

  JLabel label      = new JLabel(); 
  label.setLocation(0,0);
  label.setIcon(new ImageIcon(world.getSprite()));
  label.setVisible(true);   

  JFrame frame      = new JFrame();
  frame.setVisible(true);
  frame.setSize(783,615);
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.add(label);

  Sprite archer      = new Sprite(ImageIO.read(new 
  File("C:/Users/sfiel42/Documents/game/archer.png")));
  archer.spawn(frame,world.getSprite(),250,400);
  archer.erase(frame,world.getSprite(),orig_world.getSprite());

  }
}

回答1:


There's a couple of issues with the code that combine to make this happen. The first is that your erase method erases the wrong section. Try writing only white pixels in the erase and you'll see. What's happening is that for the spawn method you supply coordinates, but for the erase method you do not. The getMinX() and getMinY() methods don't give you the sprite coordinates, but the minimum X and Y coordinates of the image itself. For a buffered image this is always zero, because an image does not implicitly have a position; something like a label does. Here's a version that would be correct:

public void erase(JFrame frame, BufferedImage world, BufferedImage orig_world, int x, int y) throws IOException {
    for (int stepper_y = 0; stepper_y < this.image.getHeight(); stepper_y++) {
        for (int stepper_x = 0; stepper_x < this.image.getWidth(); stepper_x++) {
            int sprite_pixel = orig_world.getRGB(x + stepper_x, y + stepper_y);
            // get pixel from orginal sprite
            int sprite_alpha = (sprite_pixel >> 24) & 0xff;
            // get alpha value from original sprite
            int sprite_red = (sprite_pixel >> 16) & 0xff;
            // get red value from original sprite
            int sprite_green = (sprite_pixel >> 8) & 0xff;
            // get green value from original sprite
            int sprite_blue = sprite_pixel & 0xff;
            // get blue value from original sprite

            int pixel = (sprite_alpha << 24) | (sprite_red << 16) | (sprite_green << 8) | sprite_blue;
            // set the pixel equal to the old values
            world.setRGB(x + stepper_x, y + stepper_y, pixel);
            // place the pixel
        }
    }
}

Better yet would be to make the x and y coordinates properties of a Sprite. After all, a sprite has a position and must maintain this information. Keeping it outside of a sprite object makes no sense from an object-oriented point of view.

So adjust your class like this:

int x, y;
BufferedImage image;

public Sprite(BufferedImage image, int x, int y) throws IOException {
    this.image = image;
    this.x = x;
    this.y = y;
}

public BufferedImage getSprite() {
    return this.image;
}

public int getX() {
    return x;
}

public int getY() {
    return y;
}

Then use the sprite's coordinates in its swpan and erase methods.

// to spawn a sprite on top of another image.
public void spawn(JFrame frame, BufferedImage world) throws IOException, InterruptedException {
    for (int sprite_y = 0; sprite_y < this.image.getHeight(); sprite_y++) {
        for (int sprite_x = 0; sprite_x < this.image.getWidth(); sprite_x++) {
            int sprite_pixel = this.image.getRGB(sprite_x, sprite_y);
            int sprite_alpha = (sprite_pixel >> 24) & 0xff;
            int sprite_red = (sprite_pixel >> 16) & 0xff;
            int sprite_green = (sprite_pixel >> 8) & 0xff;
            int sprite_blue = sprite_pixel & 0xff;
            int pixel = (sprite_alpha << 24) | (sprite_red << 16) | (sprite_green << 8) | sprite_blue;
            world.setRGB(x + sprite_x, y + sprite_y, pixel);
        }
    }
}

public void erase(JFrame frame, BufferedImage world, BufferedImage orig_world) throws IOException {
    for (int stepper_y = 0; stepper_y < this.image.getHeight(); stepper_y++) {
        for (int stepper_x = 0; stepper_x < this.image.getWidth(); stepper_x++) {
            int sprite_pixel = orig_world.getRGB(x + stepper_x, y + stepper_y);
            // get pixel from orginal sprite
            int sprite_alpha = (sprite_pixel >> 24) & 0xff;
            // get alpha value from original sprite
            int sprite_red = (sprite_pixel >> 16) & 0xff;
            // get red value from original sprite
            int sprite_green = (sprite_pixel >> 8) & 0xff;
            // get green value from original sprite
            int sprite_blue = sprite_pixel & 0xff;
            // get blue value from original sprite

            int pixel = (sprite_alpha << 24) | (sprite_red << 16) | (sprite_green << 8) | sprite_blue;
            // set the pixel equal to the old values
            world.setRGB(x + stepper_x, y + stepper_y, pixel);
            // place the pixel
        }
    }
}

The variables in the for loops are relative to the sprite (sprite_y and stepper_y from 0 to height, sprite_x and stepper_x from 0 to width), and adjust the world image relative to the sprite's base coordinates (x and y).


Onto the second issue.

What's actually really happening in your current code is that you never really rendered the background and then rendered the sprite to it. That may sound weird because you're seeing it, right? But what's going on is a race condition. Java Swing works with separate threading for its rendering, meaning that when you make something visible, you're not guaranteed that it will actually have been rendered before your code continues.

It's this part here:

JLabel label      = new JLabel(); 
label.setLocation(0,0);
label.setIcon(new ImageIcon(world.getSprite()));
label.setVisible(true);   

JFrame frame      = new JFrame();
frame.setVisible(true);
frame.setSize(783,615);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(label);

Sprite archer = new Sprite(ImageIO.read(new File("C:/Users/sfiel42/Documents/game/archer.png")));
archer.spawn(frame,world.getSprite(),250,400);
archer.erase(frame,world.getSprite(),orig_world.getSprite());

The order that things are happening is actually this:

  1. Create label with the background image (the world sprite) and set it to be visible. It doesn't have a context yet so you're not actually seeing it yet.
  2. Create the frame, set it to be visible, set its size and add the label. At this point rendering of the frame will be handled by the Swing background thread. Your code now continues. But the frame hasn't been rendered yet.
  3. Read the archer sprite.
  4. Have the archer sprite spawned, meaning it overwrites some pixels of the world image that is the label's icon.
  5. Only now is the frame rendering actually completing, with the adjusted background.

You can test this by putting a sleep in your main thread between the frame code and getting the archer sprite, like this:

frame.add(label);

Thread.sleep(5000);

Sprite archer = new Sprite(ImageIO.read(new File("C:/Users/sfiel42/Documents/game/archer.png")));

Now the frame is given time to render before the sprite can adjust the background. And as a result you don't end up seeing its change.

Here's another way you can test this. Remove the above 5 second sleep again, and now add a short sleep when half the sprite has been written:

public void spawn(JFrame frame, BufferedImage world, int x, int y) throws IOException, InterruptedException {
    int orig_x = x;
    for (int sprite_y = 0; sprite_y < this.image.getHeight(); sprite_y++) {
        if (sprite_y == this.image.getHeight() / 2) {
            Thread.sleep(100);
        }

You'll likely see half the sprite with the other half missing. All of this is gonna depend on timing of the rendering thread, your computer's speed and other aspects, so the results can be unpredictable. There was a chance that you would never see your sprite to begin with, if reading the archer sprite file had been slower.

The frame and icon don't automatically update when you change something to the world image; you're writing directly to some buffered image, so the components using it have no idea something has changed and they should change their representation on screen. Call an update after the rendering:

Sprite archer = new Sprite(ImageIO.read(new File("C:/Users/sfiel42/Documents/game/archer.png")));
archer.spawn(frame, world.getSprite());
frame.repaint();
Thread.sleep(2000);
System.out.println("Erasing");
archer.erase(frame, world.getSprite(), orig_world.getSprite());
frame.repaint();

Then the final issue is the approach to rendering taken here. A sprite is erased by keeping a copy of the background and then explicitly replacing the sprite region with that of the copy if you want to remove the sprite. This is going to make things difficult once you're getting multiple sprites or trying to move them. For example, if the sprite moves between the spawn and erase calls, you won't entirely erase it.

What's usually done in 2D rendering is that you have layers, which are rendered in some given order: one or more background layers, then the sprites on top. Play around a bit with some emulators for older consoles like the SNES or MegaDrive, or arcade emulators for systems like the NeoGeo and CPS-2 (for example MAME or Kawaks). You can often disable specific layers and see how things are rendered.

For a very simple game that has to show mostly static content like, say, a chess board, rendering the background and then the sprites on top will mostly work. But for something moving faster and constantly updating frames, you could get missing sprites or flicker depending on where you are in the rendering phase when you output to the screen.

The usual solution is the use of some frame buffer: frames are rendered into some background buffer image, and only once it's ready is it allowed to be displayed on screen. Like so:

While you can do this in Swing (and AWT), it's not a very performant way. At any rate you'll want to use components that are a bit more basic instead of labels and icons, which are intended to compose graphical user interfaces. If you don't want to use existing sprite libraries and instead do things yourself, it would probably still be best to look into interfaces for hardware rendering, such as OpenGL. There's bindings for Java available.

Also check out the Game Development Stack Exchange: https://gamedev.stackexchange.com/



来源:https://stackoverflow.com/questions/43897694/why-cant-i-erase-my-sprite

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!