从零开始的2D游戏开发 —— 像素方块生成器

洗澡的时候突发奇想,2D游戏里像素是比较简单的绘画风格,但是经常除了人物外,还有一堆乱七八糟的方块比如泥土啊,墙壁啊之类的方块,这些方块如果手画,不仅不具备随机性,而且画起来也很繁琐。因此想到,能不能写一个像素方块的生成器,来生成简单的泥土,墙壁之类像素图,从而减少美工的负担呢。于是就有这篇博客了,先写一下整体思路,然后再写具体的实现。


前言:本文前部分是本人的开发流程,基本以时间顺序描述开发过程,后半部分附有效果图和程序源码,前半部分可直接跳过。部分代码受本人能力限制,显得多余累赘,别吐槽谢谢。

程序介绍:本程序使用java编写,是一个像素方块的生成器,可用来生成像素面,同时具有扩展性,可通过编写代码扩展本程序功能。


正文

这是一个简单的软件,首先使用java搭配天下第一swing框架开发,图形界面大概分为绘画区和参数区,绘画区负责生成图像,和提供预览功能,参数区顾名思义即输入各项参数。初步界面设计如下。

此部分的部分代码如下

  public void CreateJFrame() { //声明jf窗口,设置属性 JFrame jf=new JFrame(); setLayout(null); Container c=jf.getContentPane(); JPanel DrawArea=new JPanel(null); DrawArea.setBounds(5,0,800,600); DrawArea.setBorder(BorderFactory.createTitledBorder("绘画区")); JPanel PramArea=new JPanel(null); PramArea.setBounds(810,0,200,600); PramArea.setBorder(BorderFactory.createTitledBorder("参数区")); add(DrawArea); add(PramArea); setBounds(0,0,1040,650); setTitle("Pixel Generator::CSDN Blog:Zhidai_"); setVisible(true); setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); }


接下来,分两部分,一是绘画区,一是参数区,两部分分开实现后再合成一体。这里先分析参数区的需求。

参数区需要输入参数,同时将参数传输给绘画区,因此需要一个文本框实现输入功能,一个保存按钮,保存目前输入的参数,一个清空按钮,清空目前填入的参数。输入的参数大概包括,图片大小(width,length),像素大小(pixelsize),基本颜色(R,G,B),图片存放位置,新图片名称,随机数种子。(同时基本颜色提供一个取色器,随机数种子可输入或不输入)

后期还要排版,先用流布局随便排一下大概的顺序。代码就不贴了,简单的复制黏贴而已。

然后还需要获取到输入框输入的数据。写个监听事件,监听鼠标点击按钮的时候,保存下当前时刻的参数。emm也很简单的东西,代码如下

  JB_SavePramButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String temp; temp=JF_width.getText(); imageWidth=StringToInt(temp); temp=JF_length.getText(); imageLength=StringToInt(temp); temp=JF_pixelsize.getText(); pixelSize=StringToInt(temp); temp=JF_color_R.getText(); color_R=StringToInt(temp); temp=JF_color_G.getText(); color_G=StringToInt(temp); temp=JF_color_B.getText(); color_B=StringToInt(temp); temp=JF_RandomNumber.getText(); randomNumber=StringToInt(temp); } });

清空参数也是一样的,简单的代码,如下。

  JB_ClearPramButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JF_width.setText(""); JF_length.setText(""); JF_pixelsize.setText(""); JF_color_B.setText(""); JF_color_G.setText(""); JF_color_R.setText(""); JF_RandomNumber.setText(""); } });

到此,参数区基本上框架搭好了。


在我试图分析绘画区的需求的时候,发现了一个严重的问题,就是一开始绘画区是固定的,我本来希望可以在右边参数区改完立刻预览到成品,但是如果图片过大的时候可能会有问题,就是图片超出了边界,这个时候就不能很好的预览了。因此我视图通过两个窗口来解决这个问题,打开程序后,首先是参数区,填好参数之后,点击生成图片,会打开另一个窗口,展示的就是绘画区的内容,这样设计可以灵活的调整窗口的大小,但是不利于查看。

调整后的窗口发生了较大的改变,主要缩小了参数区的大小,改善了数据结构,重新写了一个类来控制绘画区的生成,同时绘画区也可以动态生成多个,方便对比。

同时将原本窗口的关闭属性修改了,这样原先关闭一个会把参数区的也关了,现在不会发生这种问题。

 setDefaultCloseOperation(DISPOSE_ON_CLOSE);

为绘画区加了个菜单栏,进行保存操作,可预见的参数区的图片名次和地址可能也会改成弹窗保存了。

 public void createJFrame(Parameter para) { JFrame jf=new JFrame(); setLayout(null); Container c=jf.getContentPane(); JMenuBar JMB_menuBar=new JMenuBar(); JMenu M_operation=new JMenu("操作"); JMenuItem MI_save=new JMenuItem("保存图片"); M_operation.add(MI_save); JMB_menuBar.add(M_operation); setJMenuBar(JMB_menuBar); setBounds(200,200,para.imageWidth+50,para.imageLength+50); setTitle("Pixel Generator::CSDN Blog:Zhidai_"); setVisible(true); setDefaultCloseOperation(DISPOSE_ON_CLOSE); }

接下来是绘画区的绘图功能,在参数里加了一块调色板,本来还以为需要自己写,后来发现awt里有JColorChooser这个东西,而且封装的相当好,一行代码就可以解决,当然如果要更好的调色板可以自己编写,不过目前这个已经可以满足需求了。下面放修改后的参数区和绘画区。

 para.color=JColorChooser.showDialog(jf,"调色板",para.color);

这个showDialog返回的是个Color类,因此声明一个Color来获取到调色板选择的颜色,三个参数分别是:父窗口,窗口标题,默认颜色


接下来是写本工具的核心功能,也就是生成像素图。

像素图的生成先确定一种基本颜色,然后在基本颜色上面random出其他相近的颜色,然后组合起来变成像素图。因此需要一个随机算法来生成。

  Random random=new Random(para.randomNumber); int widcnt=para.imageWidth/para.pixelSize; int lengcnt=para.imageLength/para.pixelSize; Color color_t; for(int i=0;i<widcnt;i++) { for(int j=0;j<lengcnt;j++) { int color_r,color_g,color_b; int offset=para.offset; color_r=Math.max(Math.min(para.color_R+(random.nextInt()%offset-offset/2),255),0); color_g=Math.max(Math.min(para.color_G+(random.nextInt()%offset-offset/2),255),0); color_b=Math.max(Math.min(para.color_B+(random.nextInt()%offset-offset/2),255),0); color_t=new Color(color_r,color_g,color_b); g2.setColor(color_t); g2.fillRect(i*para.pixelSize,j*para.pixelSize,para.pixelSize,para.pixelSize); } }

随机像素生成算法,用一个偏移量,在基本颜色上进行偏移,同时保证偏移后的数值满足RGB的要求,即属于[0,255],之后进行绘画,计算好位置和大小之后就可以画了。一个简单的随机算法。以下是不同偏移量的效果图。


程序到这里基本完成,剩下都是修饰工作。

总结:本博客主要是用java swing编写一个像素方块生成器的过程,通过简单的搭建分为两个窗口,一个是参数,一个是绘画,在参数窗口写好参数后生成绘画窗口,绘画窗口是像素的预览模式,点击保存按钮可以保存图片到指定路径。下面会放本程序效果图和完整代码。

提醒:

像素大小应是图片宽度长度的公约数;

RGB可通过调色板获取;

偏移量为像素与基本颜色的偏移量,偏移量越大,像素颜色越偏离基本颜色;

随机数种子可填可不填;

图片保存地址为地址+图片命名+格式,如F:/a.jpg为保存在F盘根目录下文件名为a,格式是jpg的图片文件。


参数限制:

图片大小:整数,是像素大小的公倍数

RGB与偏移量:整数,[0,255],大于等于0,小于等于255。

随机数种子:整数

效果图如下:

 import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.image.BufferedImage; import java.io.FileOutputStream; import java.io.IOException; import java.util.Random; import javax.imageio.ImageIO; import javax.swing.*; public class Main { public static class Parameter{ int imageWidth; int imageLength; int pixelSize; Color color=new Color(0,0,0); int color_R; int color_G; int color_B; int offset; int randomNumber; String imageFormat; String imageLocal; } public static class createMenu extends JFrame implements MouseListener{ public Parameter para=new Parameter(); public void CreateJFrame() { //声明jf窗口,设置属性 JFrame jf=new JFrame(); setLayout(null); Container c=jf.getContentPane(); JPanel PramArea=new JPanel(new FlowLayout(FlowLayout.LEFT,0,0)); JPanel JP_ImageProperties=new JPanel(new FlowLayout(FlowLayout.CENTER,10,10)); JPanel JP_ColorProperties=new JPanel(new FlowLayout(FlowLayout.LEFT,10,10)); JPanel JP_SaveProperties=new JPanel(new FlowLayout(FlowLayout.LEFT,10,10)); JPanel JP_Others=new JPanel(new FlowLayout(FlowLayout.LEFT,10,10)); PramArea.setBounds(0,0,475,250); PramArea.setBorder(BorderFactory.createTitledBorder("参数区")); JLabel JL_width=new JLabel("图片宽度:"); JLabel JL_length=new JLabel("图片长度:"); JLabel JL_pixelsize=new JLabel("像素大小:"); JLabel JL_color_R=new JLabel("R:"); JLabel JL_color_G=new JLabel("G:"); JLabel JL_color_B=new JLabel("B:"); JLabel JL_offset=new JLabel("偏移量:"); JLabel JL_ImageLocal=new JLabel("图片保存地址:"); JLabel JL_ImageFormat=new JLabel("图片格式:"); JLabel JL_RandomNumber=new JLabel("随机数种子:"); //JCB_ImageFormat.addItem("你想要添加的格式"); 可按照右边的格式来添加其他图片格式 JComboBox JCB_ImageFormat=new JComboBox(); JCB_ImageFormat.addItem("png"); JCB_ImageFormat.addItem("jpg"); JCB_ImageFormat.addItem("bmp"); JTextField JF_width=new JTextField(); JF_width.setText("800"); JF_width.setColumns(5); JTextField JF_length=new JTextField(); JF_length.setText("600"); JF_length.setColumns(5); JTextField JF_pixelsize=new JTextField(); JF_pixelsize.setText("10"); JF_pixelsize.setColumns(5); JTextField JF_color_R=new JTextField(); JF_color_R.setText("192"); JF_color_R.setColumns(3); JTextField JF_color_G=new JTextField(); JF_color_G.setText("192"); JF_color_G.setColumns(3); JTextField JF_color_B=new JTextField(); JF_color_B.setText("192"); JF_color_B.setColumns(3); JTextField JF_offset=new JTextField(); JF_offset.setText("10"); JF_offset.setColumns(3); JTextField JF_ImageLocal=new JTextField(); JF_ImageLocal.setText("F:/a.jpg"); JF_ImageLocal.setColumns(10); JTextField JF_RandomNumber=new JTextField(); JF_RandomNumber.setText("1"); JF_RandomNumber.setColumns(3); JButton JB_ClearPramButton=new JButton("清空参数"); JB_ClearPramButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JF_width.setText(""); JF_length.setText(""); JF_pixelsize.setText(""); JF_color_B.setText(""); JF_color_G.setText(""); JF_color_R.setText(""); JF_RandomNumber.setText(""); } }); JButton JB_CreateImageButton=new JButton("生成图片"); JB_CreateImageButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { String temp; temp=JF_length.getText(); para.imageLength=StringToInt(temp); temp=JF_width.getText(); para.imageWidth=StringToInt(temp); temp=JF_pixelsize.getText(); para.pixelSize=StringToInt(temp); temp=JF_color_R.getText(); para.color_R=StringToInt(temp); temp=JF_color_G.getText(); para.color_G=StringToInt(temp); temp=JF_color_B.getText(); para.color_B=StringToInt(temp); temp=JF_offset.getText(); para.offset=StringToInt(temp); para.imageFormat= String.valueOf(JCB_ImageFormat.getSelectedItem()); para.imageLocal=JF_ImageLocal.getText(); para.color=new Color(para.color_R,para.color_G,para.color_B); temp=JF_RandomNumber.getText(); para.randomNumber=StringToInt(temp); new createDrawWindow().createJFrame(para); } }); JButton JB_GetColorButton=new JButton("调色板"); JB_GetColorButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { para.color=JColorChooser.showDialog(jf,"调色板",para.color); JF_color_R.setText(String.valueOf(para.color.getRed())); JF_color_G.setText(String.valueOf(para.color.getGreen())); JF_color_B.setText(String.valueOf(para.color.getBlue())); para.color_R=para.color.getRed(); para.color_G=para.color.getGreen(); para.color_B=para.color.getBlue(); } }); JP_ImageProperties.add(JL_width); JP_ImageProperties.add(JF_width); JP_ImageProperties.add(JL_length); JP_ImageProperties.add(JF_length); JP_ImageProperties.add(JL_pixelsize); JP_ImageProperties.add(JF_pixelsize); PramArea.add(JP_ImageProperties); JP_ColorProperties.add(JL_color_R); JP_ColorProperties.add(JF_color_R); JP_ColorProperties.add(JL_color_G); JP_ColorProperties.add(JF_color_G); JP_ColorProperties.add(JL_color_B); JP_ColorProperties.add(JF_color_B); JP_ColorProperties.add(JB_GetColorButton); JP_ColorProperties.add(JL_offset); JP_ColorProperties.add(JF_offset); PramArea.add(JP_ColorProperties); JP_SaveProperties.add(JL_ImageFormat); JP_SaveProperties.add(JCB_ImageFormat); JP_SaveProperties.add(JL_ImageLocal); JP_SaveProperties.add(JF_ImageLocal); PramArea.add(JP_SaveProperties); JP_Others.add(JL_RandomNumber); JP_Others.add(JF_RandomNumber); JP_Others.add(JB_ClearPramButton); JP_Others.add(JB_CreateImageButton); PramArea.add(JP_Others); add(PramArea); setBounds(0,0,500,300); setTitle("Pixel Generator::CSDN Blog:Zhidai_"); setVisible(true); setDefaultCloseOperation(DISPOSE_ON_CLOSE); } //String转int public int StringToInt(String t) { int n=0; for(int i=0;i<t.length();i++) { n=n*10+t.charAt(i)-'0'; } return n; } @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } } public static class createDrawWindow extends JFrame implements MouseListener { Parameter para; BufferedImage bi; public void createJFrame(Parameter para_t) { para=para_t; JFrame jf=new JFrame(); setLayout(null); Container c=jf.getContentPane(); JButton SaveButton=new JButton("<html>保存图片<br>"); SaveButton.setBounds(0,0,90,26); SaveButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { try { ImageIO.write(bi,para.imageFormat,new FileOutputStream(para.imageLocal)); } catch (IOException e1) { e1.printStackTrace(); } } }); add(SaveButton); setBounds(200,200,para.imageWidth+50,para.imageLength+100); setTitle("Pixel Generator::CSDN Blog:Zhidai_"); setVisible(true); setDefaultCloseOperation(DISPOSE_ON_CLOSE); } @Override public void paint(Graphics g) { super.paint(g); bi=new BufferedImage(para.imageWidth,para.imageLength,BufferedImage.TYPE_INT_RGB); Graphics2D g2=(Graphics2D) bi.getGraphics(); // -------------------------------------------- Random random=new Random(para.randomNumber); int widcnt=para.imageWidth/para.pixelSize; int lengcnt=para.imageLength/para.pixelSize; Color color_t; for(int i=0;i<widcnt;i++) { for(int j=0;j<lengcnt;j++) { int color_r,color_g,color_b; int offset=para.offset; color_r=Math.max(Math.min(para.color_R+(random.nextInt()%offset-offset/2),255),0); color_g=Math.max(Math.min(para.color_G+(random.nextInt()%offset-offset/2),255),0); color_b=Math.max(Math.min(para.color_B+(random.nextInt()%offset-offset/2),255),0); color_t=new Color(color_r,color_g,color_b); g2.setColor(color_t); g2.fillRect(i*para.pixelSize,j*para.pixelSize,para.pixelSize,para.pixelSize); } } // -------------------------------------------- g.drawImage(bi,30,60,this); } @Override public void mouseClicked(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } } public static void main(String[] args) { new createMenu().CreateJFrame(); } }