2005年09月09日

Java 2D 可能是在 Java 程序中编写 2D 图形程序的最显著的解决方案,但它不是唯一的一个。在本文中,Java 开发者 John Carr 提出了一种优秀的备用方案 — “Java 科学对象”(Java Objects for Science(JSci)),一个开放源代码的包,它使您能够在 Swing 中创建 2D 条形图、饼形图和折线图。请在讨论论坛与本文作者和其他读者分享您对本文的心得。

对于大多数 Java 开发者,任何类型的图形开发在本质上都与 Java 2D 和 3D API 以及 java.awt.Graphics 有紧密联系。虽然 Java 2D 和 3D API 为在 Swing 中创建图形提供优秀的工具,但您并非只可以任意使用它们,当然它们也不是最容易学的。对于那些没有时间、需要或有兴趣熬夜深刻了解 java.awt.Graphics 的人,我向您推荐一个开放源代码的备用方案:JSci。

Java 科学对象(JSci)开放源代码项目是 Durham(英国 Durham)大学粒子理论中心的三年级研究生 Mark Hale 创立的。JSci 是一个包集合,包含数学和科学类。 在撰写本文时,JSci 的版本是 .87,运行在 Java 1.1.8、1.2.x 或 1.3.x 上,但将来可能为 Java 1.4 写更新版本的 JSci。这个项目的目的是以可能有助于基于科学的软件开发的最自然方式封装科学方法和原则。支持 JSci 的设计哲学是基于这样一种思想 — “直接从黑板到代码”。也就是,数学概念和构造应该以某种方式封装在代码中。在某种程度上,JSci 作为对象设计实验与作为数学库差不多。

使用 JSci,您既可以在 AWT 中也可以在 Swing 中创建简单的条形图、折线图和饼形图。JSci.swing.JBarGraph JSci.swing.JPieChartJSci.swing.JLineGraph API 组件设计得也很好,这些组件和 AWT 绘图类都遵守 MVC 体系结构。

在本文中,我将介绍 JSci.swing 包并向您展示如何使用它的类和方法创建条形图、饼形图和折线图。我们将首先看一下组成包的核心的类。

JSci.swing 包
用于在 Swing 中创建图形的类位于 JSci.swing 包中。图 1 显示了 JSci.swing 的类图表。JSci.swing 中的类,除 JImageCanvas 之外,都从 JDoubleBufferedComponent 继承。注意:JDoubleBufferedComponentJImageCanvas 都是从 javax.swing.JComponent 继承的。

我们将在下面部分详细讨论每个类的功能。

JDoubleBufferedComponent
JSci.swing 的超类是一个抽象类,被称为 JDoubleBufferedComponent。这个类相对来说比较简单,它为自己将要建立于其上的图形提供双缓冲功能。 双缓冲指出接收组件是否应该使用缓冲区来绘画。如果双缓冲被设置为 true,那么来自这个组件的所有图画都将在屏外(offscreen)绘画缓冲区完成。屏外绘画缓冲区稍后将被复制到屏幕上。根据 javadocs,Swing 绘画系统总是使用最大的双缓冲区。如果一个组件有缓冲,而且它的其中一个父组件也有缓冲,那么就使用它的父组件的缓冲区。

JDoubleBufferedComponent 依靠自己而不是 Swing 的双缓冲实现 JComponent 处理双缓冲。这为使用 JSci.swing 包的开发者提供了比只使用 Swing 更细粒度的对双缓冲的控制。

JImageCanvas
JImageCanvas 是另一个简单的(straightforward)类。它的目的是允许图像被直接添加到容器。 JImageCanvas 创建一个 java.awt.MediaTracker 实例来装入和跟踪图像。

JContourPlot
contour plot 是 3 个数字变量之间关系的二维图解表示。两个变量用于 x 轴和 y 轴,第 3 个变量 z 用于等高位。等高位被绘制成曲线;曲线之间区域的颜色编码表示内插值。

JGraph2D
JGraph2D 超类提供 2D 图形的抽象封装。JScatterGraphJLineGraph 都继承了 JGraph2D散点图是一种统计图,被绘制用来比较两个数据集。可用它来发现两个数据集之间的相关性。 折线图显示两段信息之间的关系,以及它们是如何依赖另一方发生变化的。 沿着折线图一条边的数字被称为刻度。在后面我将更详细地讨论折线图以及如何构造一个折线图。

JCategoryGraph2D
JCategoryGraph2D 超类提供 2D 图形分类的抽象封装。JBarGraphJPieChart 都继承了 JCategoryGraph2D。一个条形图由一个坐标轴和一系列带标签的水平或垂直条组成,它们显示每个条的不同值。 饼形图是一个被分成若干部分的圆形图,每部分显示一些相关信息片段的大小。饼形图被用于显示组成整体的各部分的大小。我们将在后面讨论关于构造饼形图和条形图的更多内容。

JLineGraph3D 和 JLineTrace
如果您觉得自己需要挑战,那么请尝试 JLineGraph3D。如名称所暗示的那样,JLineGraph3D 允许您向折线图中添加第三维。JLineGraph3D 不单是让您按照 x(水平)和 y(垂直)来考虑,而是让您把 z(深度)也考虑在内。JLineTrace 允许您使用鼠标侦听器跟踪 2D 折线图。

构造一个 JPieChart
要构造任何形状的图形,您都必须理解如何用一种有意义的方式来组织数据。不管是构造条形图还是构造饼形图,当涉及到数据建模时,其过程都是相同的。图 2 提供一个数据示例,它可用于创建条形图或饼形图。

图 2. 用于创建条形图或饼形图的示例数据

一个 series 表示一个数字数组。对于 JPieChart,series 可以是 float 类型或 double 类型。一个 category 表示一个 series 内的数据列标识符。对于 JPieChart,category 用 String 形式表示。当然,您希望有一个对您的观众来说有些意义的 category,这就是为什么一个城市名看起来好象适合 PieGraph.java 中饼形图示例中的每个 category 的原因所在。为简单起见,饼形图将只用一个数据 series。清单 1 分别包含 category 和 series 类变量。

清单 1. 为饼形图定义 x 轴和数据 series

// ...

public class PieGraph extends JFrame

            implements ItemListener {

    // ...

    private static final String CATEGORY_NAMES[]

            = { "London", "Paris", "New York" };

    private static final float CONSUMERS_SERIES[]

            = { 45.3f, 27.1f, 55.5f };

    // ...

}




现在,已经定义了 CONSUMERS_NAMESSALES_SERIES 数组,我们就可以开始构造饼形图。这个示例中使用的模型是 DefaultCategoryGraph2DModel。在下一部分,我将更深入地讨论该模型的工作机制。清单 2 显示如何创建一个 DefaultCategoryGraph2DModel 实例。

清单 2. 创建一个 DefaultCategoryGraph2DModel 实例

// ...

private JPieChart getPieGraph() {

    // ...

    DefaultCategoryGraph2DModel model = new DefaultCategoryGraph2DModel();

    model.setCategories(CATEGORY_NAMES);

    model.addSeries(CONSUMERS_SERIES);

    // ...

    }

// ...




在实例化 DefaultCategoryGraph2DModel 之后,category 和 series 一定包含在这个模型中。通过 setCategories(String categoryNames[]) 方法设置一个 category。因为这个模型允许多个 series,所以方法 addSeries(float series[])addSeries(double series[]) 可用于这个目的。DefaultCategoryGraph2DModel 将每个增加的 series 存储在 Vector 中。

这个 model 实例现在可用作 JPieChart 的构造器的一个输入参数。 在创建了 JPieChart 实例后,可用 setColor(int category, java.awt.Color c) 方法定义每个 category 的颜色。 清单 3 显示了如何创建 JPieChart 实例和设置 category 颜色。

清单 3. 设置饼形图扇形块颜色

// ...

public class PieGraph extends JFrame

            implements ItemListener {

// ...

private static final int LONDON      = 0;

private static final int PARIS       = 1;

private static final int NEW_YORK    = 2;

// ...

private JPieChart getPieGraph() {

    // ...

    JPieChart pieChart  = new JPieChart(model);

    pieChart.setColor(LONDON,Color.blue);

    pieChart.setColor(PARIS,Color.yellow);

    pieChart.setColor(NEW_YORK,Color.orange);

    // ...

    }

// ...




图 3 用图解形式说明了使用 PieGraph.java 创建的饼形图。当用户请求更改 category 的颜色(通过应用程序右边的列表框)时,方法 JPieChart.setColor(int category, java.awt.Color c) 被调用来实现这种更改。要使这种更改可见,在设置了新的扇形块颜色后调用 JPieChart.redraw() 方法。

图 3. PieGraph.java 生成的饼形图的输出示例

我们只讨论了 DefaultCategoryGraph2DModel 用途的一些皮毛。其它还有许多有用的方法,包括允许您隐藏 series 的方法(setSeriesVisible(int series, boolean visible))或更改 series 数据的方法(changeSeries(int series, float newSeries[]))。 我鼓励您在有空的时候更深入地研究这些类(请参阅参考资料)。

构造一个 JBarGraph
构造一个条形图首先必须象在构造饼形图中那样为数据建模。条形图,和饼形图一样,有一个数据 series 数组(或多个)和一个 category 数组。我不使用 DefaultCategoryGraph2DModel 作为条形图的模型,而是将说明如何为您的数据创建一个定制模型,以及 JBarGraph 怎样在运行时期间使用这个模型生成条形图。为使这个练习更加有趣,我将向您展示如何动态地更改 series 元素的值和重画条形图。

如图 4 所示,一个定制 category 图模型必须继承 AbstractGraphModel 并实现 CategoryGraph2DModel(参见 SalesGraphModel.java)。注意:这部分涉及的方法继承自 CategoryGraph2DModel 接口。

图 4. 创建一个定制 category 图模型

表 1 展示了 CategoryGraph2DModel 接口的方法以及 JBarGraph 如何在运行时利用模型。

表 1. CategoryGraph2DModel 接口

方法 描述
public abstract void addGraphDataListener(GraphDataListener) 添加一个侦听器
public abstract void firstSeries() 选择第 1 个数据 series
public abstract String getCategory(int i) 返回第 i 个 category
public abstract float getValue(int i) 返回第 i 个 category 的值
public abstract boolean nextSeries() 选择下一个数据 series
public abstract void removeGraphDataListener(GraphDataListener) 除去一个侦听器
public abstract int seriesLength() 返回当前 series 的长度



让我们看一下这些方法的较为详细一点的信息。

在运行时,JBarGraph 调用 public void firstSeries(),它选择第 1 个数据 series。如果您正在处理多个数据 series,将它们存储在 Vector 中是有意义的。调用的下一个方法是 public int seriesLength();这个方法返回当前 series 的长度。seriesLength() 方法返回的整型值被用于形成一个循环构造来获取每个 category 以及那个 series 的相应整型值。在这个 series 循环中,调用 public String getCategory(int i) 获取第 i 个 category 的名称,并调用 public float getValue(int i) 获取第 i 个 category 的值。(术语第 i 个是指 category 的顺序,比如第 1、第 2、第 3 等等。)为当前 series 收集了全部数据元素后,调用 public boolean nextSeries() 方法选择下一个数据 series。如果 nextSeries() 方法返回 true,那么为新数据 series 收集数据元素的过程将重新开始 — 从 seriesLength() 方法开始。当 nextSeries() 返回 false 时,模型中不再有数据 series。

BarGraph.javaSalesGraphModel 继承 AbstractGraphModel 并实现 CategoryGraph2DModel。为简单起见,将 SalesGraphModel 配置为只处理一个数据 series。如果您查看 SalesGraphModel 中的 nextSeries() 方法,您会注意到它只返回 false;这表明这个模型内只包含一个数据 series。传给 SalesGraphModel 构造器的输入参数包括 category 的名称和一个数据 series。创建 SalesGraphModelJBarGraph 实例的代码可在清单 4 的 getBarGraph() 方法中找到。

清单 4. 创建一个 JBarGraph

// ...

public class BarGraph extends JFrame

            implements ActionListener {

    // ...

    private static final String CATEGORY_NAMES[]

            = { "London", "Paris", "New York" };

    // ...

    private static final float SALES_SERIES[]

            = { 24.1f, 14.4f, 36.8f };

// ...

    private JBarGraph getBarGraph() {

       SalesGraphModel model

           = new SalesGraphModel(CATEGORY_NAMES,

                              SALES_SERIES);

       barGraph = new JBarGraph(model);

       return barGraph;

    }

// ...


图 5 显示 BarGraph.javaSalesGraphModel.java 产生的输出。您或许已注意到,所有的 category 条颜色都一样,这与饼形图示例相反,在饼形图示例中,每个 category 的颜色都不同。这是因为 JBarGraph 只允许您改变数据 series 的颜色表示,而不是 series 内的 category。

图 5. BarGraph.java 生成的条形图的输出示例

SalesGraphModel 具有允许在数据 series 内增加 category 值的功能。当用户从 BarGraph 示例选择了一个 category(通过单选按钮)并按下“Add 1 to total”按钮时,public void incrementCategoryTotal(int i) 方法被调用。这个方法把选中要增加的 category 作为方法参数传递,传递后会有一个通知被发送到 JBarGraph 实例(通过 public void dataChanged(GraphDataEvent) 方法):模型中的数据已经发生改变,必须重画此条形图。清单 5 显示了增加 category 总数的过程。

清单 5. 增加 category 总数

// ...

public class BarGraph extends JFrame

            implements ActionListener {

    private void addToCategoryTotal(int category) {

        SalesGraphModel model

             = (SalesGraphModel)barGraph.getModel();

        model.incrementCategoryTotal(category);

    }

// ...

}

// ...

public class SalesGraphModel

       extends AbstractGraphModel

               implements CategoryGraph2DModel {

    // ...

    private float seriesTotals[];

    // ...

    public void incrementCategoryTotal(int i) {

        seriesTotals[i]++;

    }

// ...

}




构造一个 JLineGraph
JLineGraph 的模型与 JPieChartJBarGraph 的模型相似;这两个模型都有多个数据 series 和一个标识 series 内数据的 category。但 JLineGraph 的模型引入了 x 轴和 y 轴坐标系的复杂性,在这一部分,我将研究如何创建一个定制的模型,并展示 JLineGraph 是如何在运行时期间使用这个模型生成一个折线图的。注意:这一部分涉及的方法继承自 Graph2DModel 接口。

一个定制的折线图模型必须要从 AbstractGraphModel 继承并实现 Graph2DModel 接口,如表 2 中所示。

表 2. Graph2DModel 接口

方法 描述
public abstract void addGraphDataListener(GraphDataListener) 添加一个侦听器
public abstract void firstSeries() 选择第 1 个数据 series
public abstract float getXCoord(int i) 返回第 i 个 category
public abstract float getYCoord(int i) 返回第 i 个 category 的值
public abstract boolean nextSeries() 选择下一个数据 series
public abstract void removeGraphDataListener(GraphDataListener) 除去一个侦听器
public abstract int seriesLength() 返回当前 series 的长度



这些方法的执行与表 1 中所示的相似。

在运行时期间,JLineGraph 调用 public void firstSeries() 方法;这选择第 1 个数据 series。调用的下一个方法是 public int seriesLength();它返回当前 series 的长度。seriesLength() 方法返回的整型值被用于形成一个循环构造以获取那个 series 的 x 轴和 y 轴坐标值。在这个 series 循环中,调用 public float getXCoord(int i) 获取第 i 个 category,并调用 public float getYCoord(int i) 获取第 i 个 category 的值。为当前 series 收集了全部数据元素后,调用 public boolean nextSeries() 方法选择下一个数据 series。如果 nextSeries() 方法返回 true,那么为新数据 series 收集数据元素的过程将重新开始 — 从 seriesLength() 方法开始。如果 nextSeries() 返回 false,那是因为模型中不再有数据 series。

缺省情况下,JLineGraph 只接受 x 轴和 y 轴的浮点值。在我的折线图中,x 轴应该包含代表一年中前六月(一月到六月)的 String。要完成这个任务,必须发生两件事:第一,必须继承 Graph2DModel 接口以包含 public String getXLabel(float i) 方法。这样您就能够获得 x 轴的 String 表示。第二,必须继承 JLineGraph,并覆盖 drawLabeledAxes(Graphics g) 方法。这一步将允许您使用新的接口访问 getXLabel(float f) 方法。图 6 是一个类图表, 显示创建一个带标签的折线图涉及的类。

图 6. 带标签折线图的类图表

创建 DemandGraphModelJLineGraph 类的实例代码可在 getLineGraph() 方法中找到,清单 6 中显示了这些代码。

清单 6. 创建一个 JLineGraph 实例

// ...

public class LineGraph extends JFrame {

    private JLineGraph lineGraph;

    private static final String MONTH_NAMES[] = { "Jan", "Feb", //... };

    private static final int MONTH_NUMBERING[] = {0, 1, 2, 3, 4, 5 };

    private static final int LONDON_SERIES  = 0;

    private static final int PARIS_SERIES   = 1;

    private static final int NEW_YORK_SERIES= 2;

    private static final float LONDON_DEMAND[] = { 1.2f, 3.7f, 6.7f, // ... };

    private static final float PARIS_DEMAND[] = { 12.6f, 15.0f, 13.7f, // ... };

    private static final float NEW_YORK_DEMAND[] = { 15.0f, 13.9f, 10.1f, // ... };

// ...

    private JLineGraph getLineGraph() {

        DemandGraphModel model = new DemandGraphModel();

        model.addSeries(LONDON_DEMAND);

        model.addSeries(PARIS_DEMAND);

        model.addSeries(NEW_YORK_DEMAND);

        model.setXAxisLabel(MONTH_NAMES);

        model.setXAxis(MONTH_NUMBERING);

        lineGraph = new LabeledLineGraph(model);

        lineGraph.setColor(LONDON_SERIES, Color.red);

        lineGraph.setColor(PARIS_SERIES, Color.yellow);

        lineGraph.setColor(NEW_YORK_SERIES, Color.blue);

        lineGraph.setXIncrement(1);

        return lineGraph;

    }

// ...

}




清单 6 的最后一行显示了对 LabeledLineGraph setXIncrement(int i) 方法的一次调用。完成这次调用是为了确保 x 轴是按照严格的增量画的。换句话说,JLabeledLineGraph 将用最大化应用程序窗口或浮动窗口的相同坐标来标记 x 轴上的每一“格”。图 7 显示了 LineGraph.java 产生的输出示例。调整应用程序窗口的大小,看看折线图是如何被重画以适合窗口新尺寸的。

图 7. LineGraph.java 生成的折线图的输出示例

结论
JSci 是一个开放源代码成果,主要用于科学应用。在本文中,我已经介绍了作为创建 2D 图形工具的 JSci 的一些可能用途。JSci 是专业绘图包的备用的免费软件。虽然 JSci 并不提供专业画图包所带的内建类型支持,您却的确可以访问源代码,并且可以自由修改代码,然后将它们提交回 JSci 社区。JSci 是一个不断发展的成果,我认为它是一个很好的备用方案,您在用 Java 创建 2D 图形时可以考虑使用它。

笔者将response内响应给使用者的内容,使用GZIP压缩的方式回传给浏览器,而IE5NS6也都有支持Gzip的压缩格式。
这个方法在之前就有人提出过,因为是让网页在输出时经过压缩,可以让传输量变小很多,虽然现在的网络频宽对于用来看网页已经绰绰有余,但是档案大小太大的网页还是会造成一定的影响。
经过Gzip压缩过的网页,档案大小可以到原本压缩前的20%

package com.jsptw.filter;
import java.io.*;
import java.util.zip.GZIPOutputStream;
import javax.servlet.*;
import javax.servlet.http.*;
public class GZIPEncodeFilter implements Filter {
      public void init(FilterConfig filterConfig) {}
      public void doFilter( ServletRequest request, ServletResponse response, FilterChain chain)
                                      throws IOException, ServletException {       
        String transferEncoding = getGZIPEncoding((HttpServletRequest)request);       
        if (transferEncoding == null)
        {           
          chain.doFilter(request, response);       
        }
        else
        {         
          ((HttpServletResponse)response).setHeader("Content-Encoding", transferEncoding);
            GZIPEncodableResponse wrappedResponse = new GZIPEncodableResponse((HttpServletResponse)response);
            chain.doFilter(request, wrappedResponse);         
            wrappedResponse.flush();       
        }   
      }   
      public void destroy() {}   
      private static String getGZIPEncoding(HttpServletRequest request) {       
        String acceptEncoding = request.getHeader("Accept-Encoding");       
          if (acceptEncoding == null)
            return null;       
          acceptEncoding = acceptEncoding.toLowerCase();       
          if (acceptEncoding.indexOf("x-gzip") >= 0)
          {
            return "x-gzip";
          }       
          if (acceptEncoding.indexOf("gzip") >= 0)
          {           
            return "gzip";       
          }       
          return null;   
      }   
     
      private class GZIPEncodableResponse extends HttpServletResponseWrapper {       
        private GZIPServletStream wrappedOut;       
          public GZIPEncodableResponse(HttpServletResponse response) throws IOException {          
            super(response);           
              wrappedOut = new GZIPServletStream(response.getOutputStream());       
          }       
          public ServletOutputStream getOutputStream() throws IOException {           
            return wrappedOut;       
          }       
          private PrintWriter wrappedWriter;       
          public PrintWriter getWriter() throws IOException {           
            if (wrappedWriter == null) {               
                wrappedWriter = new PrintWriter( new OutputStreamWriter( getOutputStream(), getCharacterEncoding()));            }           
                 return wrappedWriter;       
              }       
          public void flush() throws IOException {           
            if (wrappedWriter != null) {              
                wrappedWriter.flush();           
              }          
              wrappedOut.finish();       
          }   
      }   
     
      private class GZIPServletStream extends ServletOutputStream {       
        private GZIPOutputStream outputStream;       
          public GZIPServletStream(OutputStream source) throws IOException {           
         outputStream = new GZIPOutputStream(source);       
       }       
       public void finish() throws IOException {
         OutputStream.finish();       
       }       
      public void write(byte[] buf) throws IOException {           
        outputStream.write(buf);       
      }       
      public void write(byte[] buf, int off, int len) throws IOException {           
        outputStream.write(buf, off, len);       
      }       
      public void write(int c) throws IOException {           
        outputStream.write(c);       
      }       
      public void flush() throws IOException {           
        outputStream.flush();       
      }      
      public void close() throws IOException {           
        outputStream.close();        
      }   
    }
}


web.xml中的设定为
  :
<filter>       
<filter-name>GZIPEncoder</filter-name>       
<filter-class>com.jsptw.filter.GZIPEncodeFilter</filter-class>
</filter>

<filter-mapping> 
<filter-name> GZIPEncoder</filter-name> 
<url-pattern>/*</url-pattern>
</filter-mapping> 
  :


这个范例是笔者要在将来的书中,介绍filter机制时所要用的code
主要是为了介绍filter和filter的wrapper机制
此filter会将设定的网页范围全部用gzip压缩过,
然后浏览器将可以直接看压缩的网页

图片资源乃是游戏的外衣,直接影响一个游戏是否看上去很美。在J2ME游戏开发中,由于受到容量和内存的两重限制,图片使用受到极大的限制。在这种环境中,处理好图片的使用问题就显得更加重要。
本文从容量和内存两个方面谈谈J2ME游戏图片处理的基本方法。

一 减少图片容量

方法1:将多张png图片集成到一张图片上。
这是最基本也是最有效的减少png图片容量的办法了。比如你有10张png图片,每张10×15,现在你可以把它集成到一张100×15或者10×150或者X×X的图片上去。这张大png图片的容量比10张png图片的总容量小很多。这是因为省去了9张图片的文件头,文件结束数据块等等,而且合并了调色板(如果10张图片的调色板恰好相同,则省去了9张图片的调色板所占的容量!这是个不小的数字)

方法2:减少图片的颜色数
减少颜色也算是一个方法?我想说的是什么时候减,谁去减。如果游戏完成后发现容量超出,此时在用优化工具减少颜色,虽然能降低图片容量,但图片效果可能就不让你满意了。所以,在美工作图时就要确定使用的颜色数,手机游戏使用的是象素图,即一个象素一个象素点出来的图像,所以预先规定调色板颜色数量是可以办到的。不过,最终使用优化工具也是有用的,有时候相差一两种颜色,但效果差别并不大,容量却可以变小一些。呵呵,减少颜色确实可以算是一种方法。

方法3:尽可能使用旋转和翻转
这点不用解释了

方法4:使用换调色板技术和自定义图片格式
如果前两种方法还不能满足你对容量的要求,而你的游戏中恰好使用了很多仅颜色不同的怪物,那么可以试试换调色板技术。J2ME规范中规定手机至少可以支持png格式的图片,每张png都带有调色板数据,如果两张图片除了颜色不同而其他(包括颜色数)完全相同,则只要保存一张图片和其他图片的调色板,这相对于保存多张图片来说节省了不少容量。不过这个方法挺麻烦,你得了解png文件格式,然后做一个工具提取出调色板数据和调色板数据块在png文件中的偏移。内存中保存图像仍使用Image,如果要换调色板,则将png文件读入到一个字节数组中,根据调色板数据块在png中的偏移,用新的调色板代替原来的调色板数据,然后用这个字节数组创建出换色后的Image。也许你觉得保存一张png和n份调色板数据的方法有点浪费。至少多保存了1份调色板数据啊!如果直接将图像数据提取出来,在加上n份调色板数据,岂不是更节省容量。但是使用上面的方法,我们还可以用drawImage渲染。如果这样自定义了图片格式,那只有自己写个渲染函数了,这倒还可以,只不过put pixel的速度在某些机器上非常慢。或者自己构造png格式数据,再使用Image.如果你真得决定这么做,我还有个小建议,不要对图像数据进行压缩,zip压缩大多数时候比你写得压缩算法好(参见J2ME Game开发笔记-压缩还是不压缩)。论坛上有位朋友提过使用bmp格式代替png格式,jar中图片容量更小,也是一个道理。

二 减少图片所占内存

1 图片所占内存的计算
png图片所占用的内存并不对应于图片容量。图片占用的内存的计算为:width*height*bpp。bpp即为系统内置的颜色位数。以Nokia 6600为例,象素格式为565共16位。所以一张100*100的图片占用100*100*(16/8)=20000字节,约为19.5k的内存。象素格式是固定的无法改变,所以只有减少图片的宽和高才能降低其消耗的内存。

2 减少Image对象数量可节约大量内存
减少Image对象数量不等于减少图片数量。我的意思是说,将一张集成图保存在一个Image对象中,通过setClip的方法从这个Iamge对象中选取你需要的图像渲染。不过这个方法牺牲了一点速度,每帧都从集成图Image中减切图像的速度比无减切的渲染慢。但对于数目不多的渲染,比如精灵,使用这个方法没问题。这个方法还有一个问题就是不能释放集成图中不需要的图片,这就要看你集成的程度了。从图片容量和内存管理的角度综合考虑,我一般使用二次集成的方法。比如有n个精灵,先将各精灵所有的图片集成到一张集成图中,得到n张集成图,然后将这n张集成图再次集成到一张更大的集成图中。这样在jar中只存在一张集成图。使用时,先将大集成图分割载入到n个Image对象中即可。这样各个精灵的图片可以单独管理了。

3 使用旋转和翻转
只保存一个原始的Image,需要时再旋转或翻转

后记:
本文仅仅从图片方面谈谈容量和内存,所谈的几点均是普遍的方法,内行人一眼就能看明白,对于新手可以参考一下。减少J2ME游戏容量和内存也确实是一个值得探讨的问题,图片方面仅是其一。想要有较好的效果必须从资源代码等多方面入手,而这之中必须处理好容量,速度,内存,内存峰值,等待时间等等的关系.最后的方案往往是各方面因素相互平衡的结果.

2005年08月06日

I hope this little document will help enlighten those of you out there who want to know more about the Lempel-Ziv Welch compression algorithm, and, specifically, the implementation that GIF uses.

Before we start, here’s a little terminology, for the purposes of this document:

"character":
a fundamental data element. In normal text files, this is just a single byte. In raster images, which is what we’re interested in, it’s an index that specifies the color of a given pixel. I’ll refer to an arbitray character as "K".
"charstream":
a stream of characters, as in a data file.
"string":
a number of continuous characters, anywhere from one to very many characters in length. I can specify an arbitrary string as "[...]K".
"prefix":
almost the same as a string, but with the implication that a prefix immediately precedes a character, and a prefix can have a length of zero. So, a prefix and a character make up a string. I will refer to an arbitrary prefix as "[...]".
"root":
a single-character string. For most purposes, this is a character, but we may occasionally make a distinction. It is [...]K, where [...] is empty.
"code":
a number, specified by a known number of bits, which maps to a string.
"codestream":
the output stream of codes, as in the "raster data"
"entry":
a code and its string.
"string table":
a list of entries; usually, but not necessarily, unique.

That should be enough of that.

LZW is a way of compressing data that takes advantage of repetition of strings in the data. Since raster data usually contains a lot of this repetition, LZW is a good way of compressing and decompressing it.

For the moment, lets consider normal LZW encoding and decoding. GIF’s variation on the concept is just an extension from there.

LZW manipulates three objects in both compression and decompression:   the charstream, the codestream, and the string table. In compression, the charstream is the input and the codestream is the output. In decompression, the codestream is the input and the charstream is the output. The string table is a product of both compression and decompression, but is never passed from one to the other.

The first thing we do in LZW compression is initialize our string table.   To do this, we need to choose a code size (how many bits) and know how many values our characters can possibly take.   Let’s say our code size is 12 bits, meaning we can store 0->FFF, or 4096 entries in our string table.   Lets also say that we have 32 possible different characters.   (This corresponds to, say, a picture in which there are 32 different colors possible for each pixel.)  To initialize the table, we set code#0 to character#0, code #1 to character#1, and so on, until code#31 to character#31.   Actually, we are specifying that each code from 0 to 31 maps to a root.   There will be no more entries in the table that have this property.

Now we start compressing data.   Let’s first define something called the "current prefix".   It’s just a prefix that we’ll store things in and compare things to now and then.   I will refer to it as "[.c.]".   Initially, the current prefix has nothing in it.   Let’s also define a "current string", which will be the current prefix plus the next character in the charstream.   I will refer to the current string as "[.c.]K", where K is some character.   OK, look at the first character in the charstream.   Call it P.   Make [.c.]P the current string.   (At this point, of course, it’s just the root P.)  Now search through the string table to see if [.c.]P appears in it.   Of course, it does now, because our string table is initialized to have all roots.   So we don’t do anything.   Now make [.c.]P the current prefix.   Look at the next character in the charstream.   Call it Q.   Add it to the current prefix to form [.c.]Q, the current string.   Now search through the string table to see if [.c.]Q appears in it.   In this case, of course, it doesn’t.   Aha!  Now we get to do something.   Add [.c.]Q (which is PQ in this case) to the string table for code#32, and output the code for [.c.] to the codestream.   Now start over again with the current prefix being just the root Q.   Keep adding characters to [.c.] to form [.c.]K, until you can’t find [.c.]K in the string table.   Then output the code for [.c.] and add [.c.]K to the string table.   In pseudo-code, the algorithm goes something like this:

  1. Initialize string table;
  2. [.c.] <- empty;
  3. K <- next character in charstream;
  4. Is [.c.]K in string table?
    • yes:
      • [.c.] <- [.c.]K;
      • go to [3];
    • no:
      • add [.c.]K to the string table;
      • output the code for [.c.] to the codestream;
      • [.c.] <- K;
      • go to [3];

It’s as simple as that!   Of course, when you get to step [3] and there aren’t any more characters left, you just output the code for [.c.] and throw the table away.   You’re done.

Wanna do an example?   Let’s pretend we have a four-character alphabet:   A,B,C,D.   The charstream looks like ABACABA.   Let’s compress it.   First, we initialize our string table to:   #0=A, #1=B, #2=C, #3=D.   The first character is A, which is in the string table, so [.c.] becomes A.   Next we get AB, which is not in the table, so we output code #0 (for [.c.]), and add AB to the string table as code #4. [.c.] becomes B.   Next we get [.c.]A = BA, which is not in the string table, so output code #1, and add BA to the string table as code #5. [.c.] becomes A.   Next we get AC, which is not in the string table.   Output code #0, and add AC to the string table as code #6. Now [.c.] becomes C.   Next we get [.c.]A = CA, which is not in the table.   Output #2 for C, and add CA to table as code#7.   Now [.c.] becomes A.   Next we get AB, which IS in the string table, so [.c.] gets AB, and we look at ABA, which is not in the string table, so output the code for AB, which is #4, and add ABA to the string table as code #8. [.c.] becomes A.   We can’t get any more characters, so we just output #0 for the code for A, and we’re done.   So, the codestream is #0#1#0#2#4#0.

A few words (four) should be said here about efficiency:   use a hashing strategy.     The search through the string table can be computationally intensive, and some hashing is well worth the effort.   Also, note that "straight LZW" compression runs the risk of overflowing the string table – getting to a code which can’t be represented in the number of bits you’ve set aside for codes.   There are several ways of dealing with this problem, and GIF implements a very clever one, but we’ll get to that.  

An important thing to notice is that, at any point during the compression, if [...]K is in the string table, [...] is there also.   This fact suggests an efficient method for storing strings in the table.   Rather than store the entire string of K’s in the table, realize that any string can be expressed as a prefix plus a character:   [...]K.   If we’re about to store [...]K in the table, we know that [...] is already there, so we can just store the code for [...] plus the final character K.

Ok, that takes care of compression.   Decompression is perhaps more difficult conceptually, but it is really easier to program.

Here’s how it goes:   We again have to start with an initialized string table.   This table comes from what knowledge we have about the charstream that we will eventually get, like what possible values the characters can take.   In GIF files, this information is in the header as the number of possible pixel values.   The beauty of LZW, though, is that this is all we need to know.   We will build the rest of the string table as we decompress the codestream.   The compression is done in such a way that we will never encounter a code in the codestream that we can’t translate into a string.

We need to define something called a "current code", which I will refer to as "<code>", and an "old-code", which I will refer to as "<old>".   To start things off, look at the first code.   This is now <code>.   This code will be in the intialized string table as the code for a root.   Output the root to the charstream.   Make this code the old-code <old>.   *Now look at the next code, and make it <code>.   It is possible that this code will not be in the string table, but let’s assume for now that it is.   Output the string corresponding to <code> to the codestream.   Now find the first character in the string you just translated.   Call this K.   Add this to the prefix [...] generated by <old> to form a new string [...]K.   Add this string [...]K to the string table, and set the old-code <old> to the current code <code>.   Repeat from where I typed the asterisk, and you’re all set.   Read this paragraph again if you just skimmed it!!!   Now let’s consider the possibility that <code> is not in the string table.   Think back to compression, and try to understand what happens when you have a string like P[...]P[...]PQ appear in the charstream.   Suppose P[...] is already in the string table, but P[...]P is not.   The compressor will parse out P[...], and find that P[...]P is not in the string table.   It will output the code for P[...], and add P[...]P to the string table.   Then it will get up to P[...]P for the next string, and find that P[...]P is in the table, as the code just added.   So it will output the code for P[...]P if it finds that P[...]PQ is not in the table.   The decompressor is always "one step behind" the compressor.   When the decompressor sees the code for P[...]P, it will not have added that code to it’s string table yet because it needed the beginning character of P[...]P to add to the string for the last code, P[...], to form the code for P[...]P.   However, when a decompressor finds a code that it doesn’t know yet, it will always be the very next one to be added to the string table.   So it can guess at what the string for the code should be, and, in fact, it will always be correct.   If I am a decompressor, and I see code#124, and yet my string table has entries only up to code#123, I can figure out what code#124 must be, add it to my string table, and output the string.   If code#123 generated the string, which I will refer to here as a prefix, [...], then code#124, in this special case, will be [...] plus the first character of [...].   So just add the first character of [...] to the end of itself.   Not too bad.   As an example (and a very common one) of this special case, let’s assume we have a raster image in which the first three pixels have the same color value.   That is, my charstream looks like:   QQQ….   For the sake of argument, let’s say we have 32 colors, and Q is the color#12.   The compressor will generate the code sequence 12,32,…. (if you don’t know why, take a minute to understand it.)  Remember that #32 is not in the initial table, which goes from #0 to #31.   The decompressor will see #12 and translate it just fine as color Q.   Then it will see #32 and not yet know what that means.   But if it thinks about it long enough, it can figure out that QQ should be entry#32 in the table and QQ should be the next string output.   So the decompression pseudo-code goes something like:

  1. Initialize string table;
  2. get first code: <code>;
  3. output the string for <code> to the charstream;
  4. <old> = <code>;
  5. <code> <- next code in codestream;
  6. does <code> exist in the string table?
    • yes:
      • output the string for <code> to the charstream;
      • [...] <- translation for <old>;
      • K <- first character of translation for <code>;
      • add [...]K to the string table;
      • <old> <- <code>;
    • no:
      • [...] <- translation for <old>;
      • K <- first character of [...];
      • output [...]K to charstream and add it to string table;
      • <old> <- <code>
  7. go to [5];

Again, when you get to step [5] and there are no more codes, you’re finished.   Outputting of strings, and finding of initial characters in strings are efficiency problems all to themselves, but I’m not going to suggest ways to do them here.   Half the fun of programming is figuring these things out!  

Now for the GIF variations on the theme.   In part of the header of a GIF file, there is a field, in the Raster Data stream, called "code size".   This is a very misleading name for the field, but we have to live with it.   What it is really is the "root size".   The actual size, in bits, of the compression codes actually changes during compression/decompression, and I will refer to that size here as the "compression size".   The initial table is just the codes for all the roots, as usual, but two special codes are added on top of those.   Suppose you have a "code size", which is usually the number of bits per pixel in the image, of N.   If the number of bits/pixel is one, then N must be 2:   the roots take up slots #0 and #1 in the initial table, and the two special codes will take up slots #4 and #5.   In any other case, N is the number of bits per pixel, and the roots take up slots #0 through #(2**N-1), and the special codes are (2**N) and (2**N + 1).   The initial compression size will be N+1 bits per code.   If you’re encoding, you output the codes (N+1) bits at a time to start with, and if you’re decoding, you grab (N+1) bits from the codestream at a time.   As for the special codes:   <CC> or the clear code, is (2**N), and <EOI>, or end-of-information, is (2**N + 1).   <CC> tells the compressor to re-initialize the string table, and to reset the compression size to (N+1).   <EOI> means there’s no more in the codestream.   If you’re encoding or decoding, you should start adding things to the string table at <CC> + 2.   If you’re encoding, you should output <CC> as the very first code, and then whenever after that you reach code #4095 (hex FFF), because GIF does not allow compression sizes to be greater than 12 bits.   If you’re decoding, you should reinitialize your string table when you observe <CC>.   The variable compression sizes are really no big deal.   If you’re encoding, you start with a compression size of (N+1) bits, and, whenever you output the code (2**(compression size)-1), you bump the compression size up one bit.   So the next code you output will be one bit longer.   Remember that the largest compression size is 12 bits, corresponding to a code of 4095.   If you get that far, you must output <CC> as the next code, and start over.   If you’re decoding, you must increase your compression size AS SOON AS YOU write entry #(2**(compression size) – 1) to the string table.   The next code you READ will be one bit longer.   Don’t make the mistake of waiting until you need to add the code (2**compression size) to the table.   You’ll have already missed a bit from the last code.   The packaging of codes into a bitsream for the raster data is a potential stumbling block for the novice encoder or decoder.   The lowest order bit in the code should coincide with the lowest available bit in the first available byte in the codestream.   For example, if you’re starting with 5-bit compression codes, and your first three codes are, say, <abcde>, <fghij>, <klmno>, where e, j, and o are bit#0, then your codestream will start off like:

      byte#0: hijabcde
      byte#1: .klmnofg

So the differences between straight LZW and GIF LZW are:   two additional special codes and variable compression sizes.   If you understand LZW, and you understand those variations, you understand it all!

Just as sort of a P.S., you may have noticed that a compressor has a little bit of flexibility at compression time.   I specified a "greedy" approach to the compression, grabbing as many characters as possible before outputting codes.   This is, in fact, the standard LZW way of doing things, and it will yield the best compression ratio.   But there’s no rule saying you can’t stop anywhere along the line and just output the code for the current prefix, whether it’s already in the table or not, and add that string plus the next character to the string table.   There are various reasons for wanting to do this, especially if the strings get extremely long and make hashing difficult.   If you need to, do it.

Hope this helps out.  —-steve blackstock

2005年08月02日

简单明嘹的3D游戏开发流程
队伍组成
开发团队
n     制作人
n     执行制作人
n     策划团队
n     程式团队
n     美术团队
销售团队
测试团队
游戏评论队伍
游戏制作人
n     开发组长(always)
n     资源管理 (Resource Management)
n     行政管理 (Administration)
n     向上負責 (Upward Management)
n     专案管理 (Project Management)
游戏执行制作人
n     专案管理执行 (Project Management)
n     Daily 運作
n     House Keeping
n     Not full-time job position
游戏策划
n     故事设计 (Story Telling)
n     脚本设计 (Scripting)
n     玩法设计 (Game Play Design)
n     关卡设计 (Level Design)
n     游戏調適 (Game Tuning)
n     数值设定 (Numerical Setup)
n     AI 设计 (Game AI)
n     音效设定 (Sound FX Setup)
n     场景设定 (Scene Setup)
游戏美术
n     场景 (Terrain)
n     人物 (Character)
n     建模 (Models)
n     材質 (Textures)
n     动作 (Motion / Animation)
n     特效 (FX)
n     用户界面User Interface
游戏程序
n     游戏程序 (Game Program)
n     游戏开发工具 (Game Tools)
n     Level Editor
n     Scene Editor
n     FX Editor
n     Script Editor
n     游戏Data Exporters from 3D Software
n     3dsMax / Maya / Softimage
n     游戏引擎开发Game Engine Development
n     网络游戏服务端开发Online Game Server Development
游戏开发流程

n     创意 (Idea)
n     提案 (Proposal)
n     制作 (Production)
n     整合 (Integration)
n     测试 (Testing)
n     除錯 (Debug)
n     调试 (Tuning)
游戏设计(Concept Design)
n     游戏类型 (Game Types)
n     游戏世界观 (Game World)
n     故事 (Story)
n     游戏特色 (Features)
n     游戏玩法 (Game Play)
n     游戏定位 (Game Product Positioning)
n     Target player
n     Marketing segmentation / positioning
n     风险评估 (Risk)
n     SWOT (优势Strength/缺点Weakness/机会Opportunity/威胁Threat)
游戏提案 (Proposal)
n     系統分析 (System Analysis)
n     游戏设计文件撰写 (Game Design Document)
n     传播媒介文件撰写 (Media Design Document)
n     技术设计文案撰写 (Technical Design Document)
n     游戏专案建立 (Game Project)
n     时间表Schedule
n     进程/控制Milestones / Check points
n     管理Risk management
n     测试计划书
n     团队建立 (Team Building)
游戏开发 (Production)
n     美术量产制作
n     (建模)Modeling
n     (结构)Textures
n     (动画)Animation
n     (动作)Motion
n     (特效)FX
n     程序开发 (Coding)
n     策划数值设定
游戏整和 (Integration)
n     关卡串联 (Level Integration)
n     数值调整 (Number Tuning)
n     音效置入 (Audio)
n     完成所有美术
n     程旬与美术結合
n     (攻略)Focus Group (说明书User Study)
n     发布一些攻略截图Release some playable levels for focus group
游戏测试 (Testing)
n     Alpha(α) 测试
n     除錯 (Debug)
n     Beta (β)测试
n     数值微调
n     Game play 微调
n     对网络游戏而言 (MMOG)
n     封閉测试 (Closed Beta)
n     開放测试 (Open Beta)
n     压力(极限)测试 (Critical Testing)
n     网络游戏才有
关于Bug
n     Bug 分級 (Bug Classification)
n     A Bug
n     B Bug
n     C Bug
n     S Bug
n     Principles
n     Bug 分級从严
n     Tester(测试对象?—) vs Debugger(调试程序)

游戏系统(Game Software System)
游戏系统(Game Software System)
系统层System Layer – APIs
n     3D Graphics API
n     DirectX 9.0 SDK – Direct3D
n     OpenGL 2.0
n     2D API
n     DirectX 9.0 SDK – DirectMedia
n     Win32 GDI
n     Input Device
n     DirectX 9.0 SDK – DirectInput
n     Audio
n     DirectX 9.0 SDK – DirectSound / Direct3DSound / DirectMedia
系统层System Layer – APIs
n     3D Graphics API
n     DirectX 9.0 SDK – Direct3D
n     OpenGL 2.0
n     2D API
n     DirectX 9.0 SDK – DirectMedia
n     Win32 GDI
n     Input Device
n     DirectX 9.0 SDK – DirectInput
n     Audio
n     DirectX 9.0 SDK – DirectSound / Direct3DSound / DirectMedia
n     OpenAL
n     OS API
n     Win32 SDK
n     MFC
n     Network
n     DirectX 9.0 SDK – DirectPlay
n     Socket library
引擎层Engine Layer
n     3D Scene Management System
n     Scene Graph
n     Shaders
n     2D Sprite System
n     Audio System
n     Gamepad
n     Hotkey
n     Mouse
n     Timers
n     Network
n     DDK Interface
n     Terrain
n     Advanced Scene Management – Space Partition
n     BSP Tree
n     Octree
n     Character System
n     Motion Blending Techniques
n     Dynamics
n     Collision Detection
n     SoundFX
n     User Interface
游戏层Game Play Modula
n     NPC (Non-playable Characters)
n     Game AI
n     Path Finding
n     Finite State Machine
n     …
n     Avatar
n     Combat System
n     FX System
n     Script System
n     Trading System
n     Number System
n     …
Game Dev Tools
n     Visual C/C++
n     .net 2003
n     Visual C/C++ 6.0+ SP5
n     DirectX
n     Current 9.0c
n     NuMega BoundsChecker
n     Intel vTune
n     3D Tools
n     3dsMax/Maya/Softimage
n     In-house Tools
~~~~~~~~~~结束~~~~~~~~~
游戏分类
n     RPG (Role playing games角色扮演)
n     AVG (Adventure games冒险类)
n     RTS (Real-time strategy games既时战略)
n     FPS (First-person shooting games主视觉射击)
n     MMORPG(多人在线角色扮演)
n     SLG (战棋)
n     Simulation(模拟)
n     Sports(运动)
n     Puzzle games(解迷)
n     Table games(棋牌)

  这个错误经常出现在初学者要进行编译DirectX程序的时候,主要是因为没有将DX的库文件引用到工程中,这里需要注意,我们将DX SDK的路径设置到VC后,并不代表我们已设置好了DX SDK,在我们的DX工程中,我们还需要进行相应的设置操作,把我们所需要的库文件(DirectX SDK Library)加入到我们的工程中,要设置这个库文件有两个方法,一个是在你工程的编译选项中进行添加,另外一种可以通过代码的方法来添加(推荐)。
  命令行:#pragma comment( lib,"xxx.lib" )
  这个是VC的编译预处理指令,将其加在代码中即可。
  例如:#pragma comment( lib,"ddraw.lib" )  这句的意思是将ddraw.lib库加入到工程中进行编译。
注:此命令行不需要加分号(“;”)。

当微软每次推出一个重要的Windows版本,一般都会同时推出一个SDK(Software Development Kit),SDK中包括开发该版本Windows所需要的函数和常数定义、API函数说明文档、相关的工具和示例。SDK一般都使用C语言,但不包括编译器。SDK可以在微软的站点下载,也可以在MSDN专业版和企业版的光盘中找到。安装SDK后,使用VC++编写和编译程序就可以了。VC++中包括了SDK中的所有头文件、帮助、示例和工具,所以如果你使用的是VC++,你一般就不需要SDK了。只有当你的VC++版本比较低,如你使用的是VC++ 5.0,而想利用Win98中提供的新功能时,才需要安装SDK。从Windows 98起,Windows SDK叫Platform SDK(http://www.microsoft.com/downloads/details.aspx?FamilyId=A55B6B43-E24F-4EA3-A93E-40C0EC4F68E5&displaylang=en)。Platform SDK包括最新的Windows API(如Windows 2000的新函数)的有关声明、例子,值得下载。

    jo问:
    1、是不是vc++即可用sdk也可以用mfc?
    2、而c语言只能用sdk,不能用mfc?
    3、开发游戏可不可以用mfc类库?为什么?
    答:
    1、VC++可以使用SDK,也可以使用MFC。
    2、MFC是C++类库,在C语言中当然无法使用。
    3、开发游戏可以使用MFC库,有一个常见的游戏《拖拉机》就使用了MFC类库。如果你的编的游戏界面简单,使用MFC也可以。不过由于MFC主要是针对普通应用程序的,对游戏的支持很少,所以也有好多人编游戏时不愿意使用MFC。

2005年07月24日

 

With thanks to Kamil Saykali of the EdCenter:

This part will show how to install the glut libraries and dll’s (to download it go to http://www.xmission.com/~nate/glut.html )

1. After you have downloaded the glut.zip file (you should get the latest ver 3.7) unzip it into a folder

2. Inside the folder you should have:

glut.dll

glut32.dll

glut.h

glut.lib

glut32.lib

3. Copy both glut.dll and glut32.dll into your windows directory (windows or winnt, depends on if you are using Windows95/98 or Windows NT)

4. Copy your glut.h to:

<drive>:\<VC++ path>\include\GL\glut.h

*** put the drive where you installed VC++ instead of the <drive> ***

*** put the directory where you installed VC++ instead of the <VC++ path>

5. Copy your glut.lib and glut32.lib to:

<drive>:\<VC++ path>\lib\glut.lib

<drive>:\<VC++ path>\lib\glut32.lib

*** put the drive where you installed VC++ instead of the <drive> ***

*** put the directory where you installed VC++ instead of the <VC++ path>

6. That should be it for installed the glut libraries. The rest of this letter shows you how to setup VC++ so that you can use the glut libraries.

This will show you how to start up an opengl project and setup the setting so that you will be able to compile and run the program. This is assuming that you have already downloaded the appropriate files and installed them in the directories that there documentation tell you to. If you have not done that you need to do it before you can run or write an opengl program.

1. Start VC++ and create a new project.

2. The project has to be a "Win32 Console Application"

3. Type in the name of the project and where it will be on your hard drive.

4. Chose an empty project and click next or finish

5. First thing you need to do when the project opens up is to click on the "Project" menu item from the top.

6. Chose "Settings" (a window will come up)

7. On the left side of the window there are tabs, chose the "Link" tab

8. The string field labeled "Object/library modules" has a few lib files already set in it

9. Go to the end of this string field and enter:

opengl32.lib glut32.lib glu32.lib

10. Chose "OK" and that will include all the opengl libraries that you need

11. Now all you need to do is include <gl\glut.h> and you are ready to go

2005年07月23日

/*basics.c*/

#include <stdio.h>
#include <stdlib.h>

int mean(int a,int b)
{
  return (a + b)/2;
}

int main()
{
int i, j;
int answer;
   /* comments are done like this */
   i = 7;
   j = 9;

   answer = mean(i,j);
   printf("The mean of %d and %d is %d\n", i, j, answer);
   exit (0);
}


_____________________________________________________________________________


/* strings.c */
#include <stdio.h>
#include <string.h>

char str1[10]; /* This reserves space for 10 characters */
char str2[10];
char str3[]= "initial text"; /* str3 is set to the right size for you
                              * and automatically terminated with a 0
                              * byte. You can only initialise
                              * strings this way when defining.
                              */
char *c_ptr;   /* declares a pointer, but doesn’t initialise it. */

unsigned int len;

main()
{
 /* copy "hello" into str1. If str1 isn’t big enough, hard luck */
 strcpy(str1,"hello");
 /* if you looked at memory location str1 you’d see these byte
    values:  ‘h’,'e’,'l’,'l’,'o’,'\0′
  */

 /* concatenate " sir" onto str1. If str1 is too small, hard luck */
 strcat(str1," sir");
 /* values at str1 :  ‘h’,'e’,'l’,'l’,'o’,’ ‘,’s’,'i’,'r’,'\0′
  */

 len = strlen(str1);  /* find the number of characters */
 printf("Length of <%s> is %d characters\n", str1, len);
 
 if(strcmp(str1, str3))
    printf("<%s> and <%s> are different\n", str1, str3);
 else
    printf("<%s> and <%s> are the same\n", str1, str3);

 if (strstr(str1, "boy") == (char*) NULL)
    printf("The string <boy> isn’t in <%s>\n", str1);
 else
    printf("The string <boy> is in <%s>\n", str1);

 /* find the first `o’ in str1 */
 c_ptr = strchr(str1,’o');

 if (c_ptr == (char*) NULL)
   printf("There is no o in <%s>\n", str1);
 else{
   printf("<%s> is from the first o in <%s> to the end.\n",
                c_ptr, str1);
   /* Now copy this part of str1 into str2 */
   strcpy(str2, c_ptr);
 }
}

_____________________________________________________________________________
/*array.c*/
#include <stdio.h>
#include <stdlib.h>

void list_names(char (*names)[10] ){
  for (; names[0][0]; names++){
    printf("%s\n", *names);
  }
}

void list_names2(char *names[] ){
  for (; *names!=NULL; names++){
    printf("%s\n",*names);
  }
}

int main(int argc, char *argv[]){
char fruits[4][10] = {"apple", "banana", "orange", ""};
char *veg[] =  {"artichoke", "beetroot", "carrot", (char*) NULL};

  list_names(fruits);
  list_names2(veg);
  exit(0);
}

_____________________________________________________________________________
/* mallocing.c */
#include <stdio.h>
#include <stdlib.h>
char* make_reverse(char *str)
{
int i;
unsigned int len;
char *ret_str, *c_ptr;
len = strlen(str);

 /* Create enough space for the string AND the final \0.
  */
 ret_str = (char*) malloc(len +1);
 /*
    Now ret_str points to a `permanent’ area of memory.
  */

 /* Point c_ptr to where the final  ‘\0′ goes and put it in */
 c_ptr = ret_str + len;
 *c_ptr = ‘\0′;

 /* now copy characters from str into the newly created space.
    The str pointer will be advanced a char at a time,
    the cptr pointer will be decremented a char at a time.
  */
 while(*str !=0){ /* while str isn’t pointing to the  last ‘\0′ */
   c_ptr–;
   *c_ptr = *str;
   str++; /* increment the pointer so that it points to each
            character in turn. */
 }
 return ret_str;
}

void main()
{
char input_str[100]; /* why 100? */
char *c_ptr;
  printf("Input a string\n");
  gets(input_str);  /* Should check return value */
  c_ptr = make_reverse(input_str);
  printf("String was %s\n", input_str);
  printf("Reversed string is %s\n", c_ptr); 
}
_____________________________________________________________________________File I/O under Unix Some file operations work on file pointers and some lower level ones use small integers called file descriptors (an index into a table of information about opened files). The following code doesn’t do anything useful but it does use most of the file handling routines. The manual pages describe how each routine reports errors. If errnum is set on error then perror can be called to print out the error string corresponding to the error number, and a string the programmer provides as the argument to perror.  #include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>    /* the man pages of the commands say which
                         include files need to be mentioned */
#define TRUE 1
int bytes_read;
size_t  fp_bytes_read;
int fd;   /* File descriptors */
int fd2;
FILE *fp; /* File pointers */
FILE *fp2;
char buffer[BUFSIZ]; /* BUFSIZ is set up in stdio.h */

main(){

  /* Use File descriptors */
  fd = open ("/etc/group", O_RDONLY);
  if (fd == -1){
     perror("Opening /etc/group");
     exit(1);
  }

  while (TRUE){
     bytes_read = read (fd, buffer,BUFSIZ);
     if (bytes_read>0)
        printf("%d bytes read from /etc/group.\n", bytes_read);
     else{
        if (bytes_read==0){
          printf("End of file /etc/group reached\n");
          close(fd);
          break;
        }
        else if (bytes_read == -1){
          perror("Reading /etc/group");
          exit(1);
        }
     }
   }


 /* now use file pointers */
 fp = fopen("/etc/passwd","r");
 if (fp == NULL){
    printf("fopen failed to open /etc/passwd\n");
    exit(1);
  }

 while(TRUE){
    fp_bytes_read= fread (buffer, 1, BUFSIZ, fp);
        printf("%d bytes read from /etc/passwd.\n", fp_bytes_read);
    if (fp_bytes_read==0)
       break;
 }

 rewind(fp); /* go back to the start of the file */

/* Find the descriptor associated with a stream */
  fd2 = fileno (fp);
  if (fd2 == -1)
    printf("fileno failed\n");

/* Find the stream associated with a descriptor */
  fp2 = fdopen (fd2, "r");
  if (fp2 == NULL)
    printf("fdopen failed\n");
  fclose(fp2);
 }
To take advantage of unix’s I/O redirection it’s often useful to write filters: programs that can read from stdin and write to stdout. In Unix, processes have stdin, stdout and stderr channels. In stdio.h, these names have been associated with file pointers. The following program reads lines from stdin and writes them to stdout prepending each line by a line number. Errors are printed on stderr. fprintf takes the same arguments as printf except that you also specify a file pointer. fprintf(stdout,….) is equivalent to printf(….).  /* line_nums.c
   Sample Usage :    line_nums < /etc/group
 */
#include <stdio.h>
#include <stdlib.h>
#define TRUE 1
int lineno = 0;
int error_flag = 0;
char buf[BUFSIZ]; /* BUFSIZ is defined in stdio.h */

main(){
  while(TRUE){
    if (fgets(buf,BUFSIZ, stdin) == NULL){
       if (ferror(stdin) != 0){
          fprintf(stderr,"Error during reading\n");
          error_flag = 1;
       }
       if (feof(stdin) != 0)
          fprintf(stderr,"File ended\n");
       clearerr(stdin);
       break; /* exit the while loop */
    }
    else{
       lineno++;
       /* in the next line, "%3d" is used to restrict the
          number to 3 digits.
       */
       fprintf(stdout,"%3d: ", lineno);
       fputs(buf, stdout);
    }
  }

  fprintf(stderr,"%d lines written\n", lineno);
  exit(error_flag);
}
ferror() and feof() are intended to clarify ambiguous return values. Here that’s not a problem since a NULL return value from fgets() can only mean end-of-file, but with for instance getw() such double checking is necessary.

2005年07月09日

My first experience using a computer was in 1981 on a Sinclair ZX Spectrum. The first 5 years of my computing life were spent on nothing but writing and modifying games for the Sinclair and later the Commodore 64, but, heck, what else are you going to do as a teenager? While much has changed in terms of hardware capabilities and available APIs, the properties of a great game have not.

Games today have become so complex that they require large numbers of developers, graphic artists, testers and managerial overhead to develop. They rival large commercial enterprise application in their complexity and cost many millions of dollars to develop and market. The payback, however, can be enormous and rival Hollywood blockbuster movies in sales – Halo 2 grossed $100M in its first day of availability.  

All successful games have a couple of features in common that made them stand out.

  • The main ingredient for a successful game is the game idea. Regardless how cool your graphics are, how good the music is, if the idea is lame no one is going to play the game.
  • The second most important feature is the playability of the game. If the game is too hard then players are quickly going to get frustrated and stop playing. Conversely, if the game play is too easy then the player is going to get bored and stop playing. A good game provides multiple levels of difficulty that continuously challenge the player without overwhelming or boring them.
  • Together, the game idea and its playability are the “game design” (not to be confused with “level design,” which is the application of the overall game design to specific segments of the game). There are certain game designers who have a golden touch. Shigeru Miyamoto (the creator of Donkey Kong, Zelda, and Mario) and Will Wright (Sim-everything) are two prominent examples. Miyamoto’s keynote address to the 1999 Game Developer’s Conference is available at http://www.gamasutra.com/features/20030502/miyamoto_01.shtml and Wright’s recent discussion of the design philosophy of Spore (http://www.gamespy.com/articles/595/595975p1.html?fromint=1) are good inspirations for designers of all stripes, while the book “Theory of Fun for Game Design” by Raph Koster has gotten excellent reviews in the community.
  • The third ingredient to a successful game is the set of graphics. They need to be good enough to compliment the game idea and game play but not so resource intensive or flashy that they distract from it.
  • The final ingredient is performance. No one wants to play a slow game. I still remember an adventure game on my Commodore64 that took 10 minutes to render each scene. I still played it, mind you, because the game idea was great and there were no other options around but it was irritating.  Graphics and performance are closely related. The more fancy graphics you add to a game to slower the performance. The next biggest performance issue is the AI. A lot of game development today focuses on how to make things faster and not coming up with new ideas. However, when you’re learning a complex programming technique such as game programming, it’s vitally important not to optimize prematurely. An understanding of the performance pipelines, and the skills to write clean code, profile it, and improve it are much more important than any single optimized function.  

If you apply your design efforts in this order you, too, can create a great game. It may not be a refined first person shooter like Battlefield 1942, but Tetris is arguably one of the most popular games and has neither fancy 3D graphics nor Dolby digital sound. Even today, games like Gish (http://www.chroniclogic.com/index.htm?gish.htm) demonstrate what can come from creative independent developers. If you can write enough of a game to show your game idea, then maybe you can interest the large gaming companies in your game. The Independent Games Festival is the “Sundance” of the game community, runs concurrently with the professional Game Developers Conference and, believe me, is among the most closely-watched events at the show.