为什么我不能删除我的 Sprite

Why can't I erase my Sprite

为了在 Java 中运行游戏,我已经尝试了一段时间,在对其他人的 Sprite 功能感到非常痛苦之后,我自己制作了游戏,但不明白为什么我不能删除它.我知道它正在更改背景的像素以显示我的弓箭手精灵,因为它出现了,但无论出于何种原因,我都无法将像素更改回之前的状态。有谁知道为什么会这样或我该如何解决? Link 到 google 带图像的文档: 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());

  }
}

代码中有几个问题共同导致了这一点。首先是您的擦除方法擦除了错误的部分。尝试在擦除中只写入白色像素,您就会看到。发生的情况是,对于 spawn 方法,您提供坐标,但对于 erase 方法,您不提供。 getMinX()getMinY() 方法不会给你精灵坐标,而是图像本身的最小 X 和 Y 坐标。对于缓冲图像,这始终为零,因为图像没有隐含的位置;像标签一样的东西。这是一个正确的版本:

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
        }
    }
}

更好的方法是制作 Sprite 的 x 和 y 坐标属性。毕竟,精灵是有位置的,必须维护这个信息。从面向对象的角度来看,将它保留在 sprite 对象之外是没有意义的。

所以像这样调整你的class:

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;
}

然后在其 swpan 和 erase 方法中使用精灵的坐标。

// 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
        }
    }
}

for循环中的变量是相对于精灵的(sprite_y和stepper_y从0到高度,sprite_x和stepper_x从0到宽度),并根据精灵的基本坐标(x 和 y)调整世界图像。


进入第二期。

您当前代码中真正发生的事情是您从未真正渲染过背景,然后再将精灵渲染到它。这听起来可能很奇怪,因为你正在看到它,对吧?但是发生的事情是竞争条件。 Java Swing 使用单独的线程进行渲染,这意味着当您使某些东西可见时,您不能保证它会在您的代码继续之前实际被渲染。

这里是这部分:

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. 使用背景图像(世界精灵)创建标签并将其设置为可见。它还没有上下文,所以您实际上还没有看到它。
  2. 创建框架,将其设置为可见,设置其大小并添加标签。 此时帧的渲染将由 Swing 后台线程处理。您的代码现在继续。但是框架还没有渲染。
  3. 阅读弓箭手精灵。
  4. 生成弓箭手精灵,这意味着它会覆盖作为标签图标的世界图像的一些像素。
  5. 直到现在帧渲染才真正完成,并调整了背景。

您可以通过在帧代码和获取弓箭手精灵之间的主线程中睡眠来测试它,如下所示:

frame.add(label);

Thread.sleep(5000);

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

现在在 sprite 可以调整背景之前,帧有时间渲染。结果你并没有看到它的变化。

这是您可以测试的另一种方法。再次去掉上面的5秒sleep,现在在sprite写到一半的时候再添加一个短暂的sleep:

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);
        }

您可能会看到一半的 sprite,另一半不见了。所有这一切都将取决于渲染线程的时间、计算机的速度和其他方面,因此结果可能无法预测。如果读取弓箭手 sprite 文件的速度较慢,您可能一开始就看不到您的 sprite。

当您更改世界图像时,框架和图标不会自动更新;您正在直接写入一些缓冲图像,因此使用它的组件不知道发生了什么变化,它们应该改变它们在屏幕上的表示。渲染后调用更新:

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();

那么最后一个问题是这里采用的渲染方法。通过保留背景的副本来擦除精灵,然后如果要删除精灵,则用副本的区域显式替换精灵区域。一旦您获得多个精灵或试图移动它们,这将使事情变得困难。例如,如果精灵在生成和擦除调用之间移动,您将不会完全擦除它。

通常在 2D 渲染中做的是你有层,这些层按给定的顺序渲染:一个或多个背景层,然后是顶部的精灵。尝试使用一些适用于 SNES 或 MegaDrive 等较旧游戏机的模拟器,或适用于 NeoGeo 和 CPS-2(例如 MAME 或 Kawaks)等系统的街机模拟器。您通常可以禁用特定图层并查看渲染效果。

对于一个必须显示大部分静态内容(例如棋盘)的非常简单的游戏,渲染背景然后顶部的精灵将大部分工作。但是对于移动速度更快且不断更新帧的物体,当您输出到屏幕时,您可能会丢失精灵或闪烁,具体取决于您在渲染阶段的位置。

通常的解决方案是使用一些帧缓冲区:帧被渲染到一些背景缓冲区图像中,只有准备好后才能在屏幕上显示。像这样:

虽然您可以在 Swing(和 AWT)中执行此操作,但这并不是一种高效的方法。无论如何,您将希望使用更基本的组件而不是标签和图标,它们旨在组成图形用户界面。如果您不想使用现有的 sprite 库而是自己动手做,那么最好还是研究一下硬件渲染的接口,例如 OpenGL。有 Java 可用的绑定。

另请查看游戏开发堆栈交换:https://gamedev.stackexchange.com/