Monday, December 28, 2009

足球小将—只不过是热血漫画

几天来把《足球小将》看了一遍。
以前基本上看得很少,只在《画书大王》上看过开头的几节,后来内地的单行本(当时应该是没授权的)买过一本。那时候觉得还不算离谱,情节设计上还不错。
几年前看了世青篇,开始觉得这个漫画已经向魔幻方向发展了,虽然情节上还是不错的,但是作者对于足球本身或者是真实的比赛的了解实在是离现实太远——好好的足球场成了格斗场和马戏场。而且你永远搞不清楚足球场究竟有多大,某个队员究竟在球场的什么位置。
在描写比赛的情节中,高桥阳一的很多设计是非常失败的。任何球队的教练从来不会作出有效的战术设计,只知道加油鼓励和说几句空话。所有的优秀队员性格都是一样,阳光的热血青年,胜不骄败不馁。
高桥讲故事的能力也值得商榷,日式漫画经常通过构图和对话来表现情节,一般极少出现大量旁白,而高桥在描写比赛的时候大量依赖旁白,这几乎是明显的港式风格,与同是体育类漫画大师的井上相比功力不可同日而语。
高桥的风格不是写实一派,人物除了发型和体型几乎很难区别。在世青篇及之后,高桥的画功似乎出现了严重的问题,人物的头明显偏小,多数人的腿部发育过度。
《足球小将》不容质疑是一部经典之作,在它诞生的时代是史无前例的。但是对于懂一些足球球迷来说,或者对于十几年之后的体育类漫画发展状况来说,还是把它当做一部纯粹的青春成长热血漫画好了。也许你把它当成类似《圣斗士星矢》的漫画会好些,热血的段子很适合煽情。可以喜欢其中的人物,其中的精神,但是无法认同其中的体育。

Wednesday, December 23, 2009

空虚的时光


想用力感觉用不到点上。


太艰苦了。


Wednesday, December 09, 2009

超级马里奥兄弟的零碎回忆

Super Mario Brothers
超级马里奥兄弟
142.超级马莉1无敌版_001

也可以翻译成“超级玛丽”之类的,虽然那个大胡子跟玛丽这个女名明显沾边。路易这个名字就这样被完全忽视了。
《超级马里奥》这个游戏似乎是很难的,其实评论一个游戏的难度还是要有些顾虑的。比如像《魂斗罗》这种游戏,死掉一条命当场就可以重来,而《超级马里奥》必须从关卡的最开始或者中间某个固定的地方重来。通关《魂斗罗》的人在大街上一抓一把,但是通关《马里奥》的恐怕就没那么多,这与游戏本身死掉一命时的处理方法关系是非常大的。
正宗的超级马里奥一代究竟有多少改版,现在已经弄不清楚了。确实存在的应该有:
黑夜版,地面为发泡样子的;
梦幻版,画面有一半左右是幻影的。
马里奥这个游戏有很多细节和传言是应该值得注意的。例如:
游戏中第3关和第6关是黑天的。
游戏中除了所有第4小关和第8关的全部外,每个小关都有一个中间的复生地点。
传说每个小关都有一个加命的蘑菇,这无疑是一个欺骗广大少年儿童的可怕谎言。根据现在网上的高手总结,全部的加命蘑菇在这里:http://tieba.baidu.com/f?kz=138936718
8-1关是最长的。
小时候我很害怕那些扔锤子的绿色动物(似乎是鸭子),因为非常难对付,背上似乎还长着倒刺,很难直接踩到。
超级马里奥2
究竟有几个超级马里奥2?这件事情看来谁也说不清,因为甚至有不少人认为FC上的《水管工马里奥》是1,而《超级马里奥》才是2。
当然这并不是什么大问题,究竟哪个是超级马里奥2也没关系。有一部更换过全部关卡,路易跳的远比马里奥高的官方发行版,目前被认为是正宗的2代。这个版本里面有一种蘑菇是有毒的。由于关卡的重新布置,难度也远比1代要大。
马2选关_001

你看到标题上大大的"2"了吗?
但是下面这个?
Super Mario Bros. 2 (U) (PRG1) [!]_001

N社你的2也不是随处乱放的。这个版本似乎在某些地区被称为4,这基本是完全的胡扯。在这款游戏里面,你可以选择4个角色。马里奥毫无特技,路易弹跳惊人,蘑菇头力气大,公主可以漂浮。在地上蹲一会可以跳高,必须拔东西才能攻击。这一代里面生命的增加也显得小气,通过魔法门可以找到蘑菇,好吧除了头两关的我基本不知道别的蘑菇在哪里。
超级马里奥3
Super Mario Bros. 3 (U) (PRG0) [!]_001

这是一个疯狂的,杀人不见血的,不要命的游戏。
超级马里奥3是N社历史上经典中的经典。不要说N社的东西都是经典,他们炮制出的垃圾绝不比任何一家烂公司少,但是N社的强大就在于惊人的骗钱能力。
扯远了。超级马里奥3的流程非常之长,共8大世界,如果完全打完大概需要数个小时。马里奥在这次有很多装备,比如浣熊,铁锤,青蛙之类。这一代中的秘密非常之多。说不定什么时候就能顶到一些不靠谱的东西。马里奥的头再次有效了,而且还能飞了。
这几部作品在SFC上均有复刻,某些操作变得简单了。无疑是在骗钱。
这个系列在FC上的回忆暂且中止,如果想玩的话,推荐使用Vnes模拟器。Nestopia对马里奥一代的支持似乎有些问题。金手指请多多使用。

SDL & Object Pascal (Delphi) [3] 制作一个选单

一个选单大致是这样:

1.可以被以某种形式呼叫出来,如按下esc;
2.在这个选单中功能键有不同的定义,如原本方向键控制走路,但现在改为控制光标的位置;
3.按下确定键后有对应功能被执行;
4.可以被关闭。

那么我的办法是编写两个子程,其中一个处理选单中的事件,另外一个专门负责画选单。

在从步行切换到选单的时候,可能需要清除当前的键值。如果步行中使用了SDL_EnableKeyRepeat方法把键盘的频率变高,对于选单来说就太快了。所以可能要在调用选单之前写上:

SDL_EnableKeyRepeat(0, 0);
event.key.keysym.sym := 0;

这样降低了键盘的反应,同时清除当前的键值,要不然呼出选单之后它可能还会自己转一会(如果之前用了PollEvent方式,我还没搞清楚原因)。不过如果在行走中使用的是WaitEvent方式,事情就会简单很多。PollEvent方式看来是太快了,在选单中不合适。

处理选单事件的子程:

procedure MenuSystem;
var
  menu: integer;
begin
  while (SDL_WaitEvent(@event) >= 0) do
  begin
    case event.type_ of
      SDL_QUITEV: //这里处理窗口退出事件
        if messagebox(0, 'Are you sure to quit?', 'KYS Windows', MB_OKCANCEL) = IDOK then Quit;
      SDL_KEYUP:
        begin
          if (event.key.keysym.sym = sdlk_down) then
          begin
            menu := menu+1;
            if menu > 3 then menu := 0; //按下下键的溢出
            showMenusystem(menu); //每次当前选中发生变化时, 均重画选单
          end;
          if (event.key.keysym.sym = sdlk_up) then
          begin
            menu := menu-1;
            if menu < 0 then menu := 3; //按下上键的溢出
            showMenusystem(menu);
          end;
          if (event.key.keysym.sym = sdlk_escape) then
          begin
            break; //按下退出键
          end;
          if (event.key.keysym.sym = sdlk_return) or (event.key.keysym.sym = sdlk_space) then
          begin //按下确定键
            case menu of
              2: MenuQuit;
              1: MenuSave;
              0: Menuload;
            end;
          end;
        end;
    end;
  end;
end;

显示选单的子程,里面有一些其他的东西,不必太在意:

procedure ShowMenuSystem(menu: integer);
var
  word: array[0..2] of Widestring;
  i: integer;
begin
  Word[0] := ' 读取';
  Word[1] := ' 存档';
  Word[2] := ' 退出';
  if fullscreen = 1 then Word[2] := ' 窗口';
  ReDraw; //你需要自己写一个清屏子程
  for i := 0 to 3 do
    if i = menu then
    begin
      drawtext(screen, @word[i][1], 64, 32+22*i, $FFFFFF)); //当前的项显示为不同的颜色
    end
    else begin
      drawtext(screen, @word[i][1], 64, 32+22*i, $00FFFF);
    end;
  SDL_UpdateRect(screen, 80, 30, 47, 93); //根据区域而定,如果觉得麻烦就更新全屏
end;

SDL & Object Pascal (Delphi) [2] 显示中文字符

一些基础的问题我不再赘述了,查阅帮助文档(Object Pascal SDL Doc.chm)显然比在这看我胡扯合适得多。那些画像素,获取像素信息,显示BMP文件多数时候只要把那些代码复制过来基本就能工作。下面说的是怎样显示中文。

其实SDL加上ttf支持时(需要将对应的dll文件添加到工程目录里)是完全可以显示中文的,简体和繁体都没问题(Object Pascal版本也不缺功能),但是只能用Unicode编码。这就是说,你必须选择一个Unicode编码完整的字体文件(或者说至少你需要的那些字符是完整的)。Windows下面这样的字体有很多,我推荐的有:

简体中文:宋体大字符集,微软雅黑;

繁体中文:细明体,微软正黑,标楷体。

这些基本是中文地区最重要的几种字体。一个缺憾就是用于显示简体中文的楷体(和其他字体)优秀的并不多。

输出中文时,可以先写这样的一个子程作为基础:

procedure DrawText(word: PUint16; x_pos, y_pos: integer; color: Uint32);
var
  text: PSDL_Surface; 
  dest: TSDL_Rect;
begin 
  text := TTF_RenderUNICODE_blended(font, word, TSDL_Color(Color));
  dest.x := x_pos;
  dest.y := y_pos;
  SDL_BlitSurface(text,nil, screen, @dest);
  SDL_FreeSurface(text);
end;

screen就是已经定义好的那个主屏幕的标记。font是一个指向字体文件的指针,可以这样定义和初始化:

全局变量:
font: PTTF_Font;

在初始化状态的子程中:
TTF_Init;
font:=TTF_OpenFont('kaiu.ttf', 20);

当然font的定义和初始化写在DrawText里面也一样,只是写在全局变量里面就可以只初始化一次,可能会效率高一些。同样把DrawText里面用的那个text写成全局变量也是一个选择。在程序结束时,还要释放这些资源。

kaiu就是标楷体的文件名字,20是字号,需要把字体文件也放在工程目录里面。

TTF_RenderUNICODE_blended这个函数是以blended效果输出文字,以TTF_RenderUNICODE开头的函数有3个,对应的是不同的效果。

SDL_BlitSurface大致可以理解为在“表面”上再贴一层。

调用DrawText的方法如下:

str: WideString;
......

str:=' 这是一个显示中文的测试';
DrawText(@str[1], 100, 100, $FFFFFF);
Sdl_UpdateRect(screen, 0, 0, screen.w, screen.h);

注意一定要用WideString(或PWideChar)类型!

str前面我添加了一个空格。这是因为我发现有些字体中的某些字符似乎包含自动缩进(原因不明),它们在作为首字符时整个字符串会右移,于是我干脆把首字符全都设成了空格(如果有人知道原因以及更好的解决方案,还请不吝赐教)。100,100,$FFFFFF就是位置和颜色了,这里我是随便写了一个颜色(应该是白的)。最后那句就是更新屏幕上的一个矩形区域了,不更新是不会有效果的。

如果用WideString类型的话,str的第0位保存的是字串长度,所以用的是str[1],由于DrawText的第一个参数是指针所以加了@。这个调用方法似乎有点别扭,当然如果在DrawText里多写几行代码也可以有其他的封装方案。

这样做基本上显示中文就可以了。但是有时你可能面对已经做好的资源文件,里面使用了其他的编码方案,比如繁体地区常用的Big5码。这时需要用到一个API函数 MultiByteToWideChar 。

这个API的参数还是有点麻烦的,可以写一个函数将它封装:

function Big5ToUnicode(str:PChar): widestring;
var
  len: integer;
begin
  len:=MultiByteToWideChar(950,0,PChar(str),-1,nil,0);
  setlength(result,len-1);
  MultiByteToWideChar(950, 0, PChar(str), -1, pwidechar(result), len+1);
  result:=' '+result;
end;

MultiByteToWideChar(950, 0, PChar(str), -1, pwidechar(result), len+1) 各个参数含义:

950:码表,950即Big5和Unicode的对应表;
0:这一位写0就可以了;
PChar(str):字符串的地址(这里又用PChar强行转换了一次,应该没必要);
-1:字符串长度,如果是-1就会自动计算长度并转换到字符串结束;
pwidechar(result):保存结果的地址;
len+1:缓冲区大小,如果为零函数就返回需要的缓冲区大小。

在Big5ToUnicode函数中MultiByteToWideChar使用了两次,第一次是计算所需缓冲区大小,第二次才是正式的转换。结果的长度设成多少需要实验一下,因为结尾或许有不想要的字符。但是不能不设或设成0,这样result并没有获得地址分配,转换会出错。至于后面在结果前又加了个空格,就看效果而定了。

在这里形参并没有用String而是PChar,是因为从资源文件中读出的数据不一定是读到String里面,甚至可能读到整数数组里,使用@算符传入地址就可以了。后面还可以利用这两个函数再写一个DrawBig5Text。

如果是GB2312,码表号是936。大部分的转码软件都是使用了这个API,GB2312与Big5的互相转换也是通过Unicode进行的。

同样还有一个WideCharToMultiByte的API,用法是类似的(Char和WideChar的位置当然相反),但是后面的参数多了两个,如果不知是干什么的可以都写成nil。

这个API是Windows提供的,所以这是在Windows下面比较简单的实现方法。但是如果作跨平台开发就不适合了。

最后是整个程序的代码。需要注意的是这份代码并未对打开设备失败进行处理。在你正式编写一个游戏时,判断一下经常是必要的。

unit myfunctions;

interface

uses
  sysutils,
  windows,
  sdl,
  sdl_ttf;

procedure Run;
procedure DrawText(word: PUint16; x_pos, y_pos: integer; color: Uint32);

implementation

var
  screen: PSDL_Surface;
  event: TSDL_event;
  font: PTTF_Font;

procedure Run;
var
  str: WideString;
begin
  SDL_Init(SDL_INIT_VIDEO);
  screen := SDL_SetVideoMode(640, 480, 32, SDL_HWSURFACE or SDL_DOUBLEBUF);
  TTF_Init;
  font := TTF_OpenFont('kaiu.ttf', 20);
  str := ' 这是一个显示中文的测试';
  DrawText(@str[1], 100, 100, $FFFFFF);
  Sdl_UpdateRect(screen, 0, 0, screen.w, screen.h);
  while SDL_PollEvent(@event) >= 0 do
  begin
    if event.type_ = SDL_QUITEV then break;
    Sdl_delay(10);
  end;
  TTF_Quit;
  SDL_Quit;
  exit;
end;

procedure DrawText(word: PUint16; x_pos, y_pos: integer; color: Uint32);
var
  text: PSDL_Surface;
  dest: TSDL_Rect;
begin
  text := TTF_RenderUNICODE_blended(font, word, TSDL_Color(Color));
  dest.x := x_pos;
  dest.y := y_pos;
  SDL_BlitSurface(text, nil, screen, @dest);
  SDL_FreeSurface(text);
end;

end.

SDL & Object Pascal (Delphi) [1] 配置,第一个视频窗口,关闭

在Delphi下面配置SDL是非常简单的,因为有一个组织(JEDI)连安装程序都做好了。

你可以到这个网站http://www.delphi-jedi.org/,下载一个JEDI-SDL的安装程序。文件的名字是[JEDI-SDLFullSetup.exe],安装就是了。当然你事先要有一个Delphi,我用的是7,其他版本的我还真不清楚。

不过要想你的游戏能正常运行,你还要去下载SDL的DLL文件,这个建议你到SDL的主页去看看。如果需要image,ttf,mixer等其他的支持,可以到这里http://www.libsdl.org/projects/。之后你要把你用到的部分放到你的游戏目录里面。当然嫌麻烦的话放到系统目录里面应该也可以,不过据说现在很多人不喜欢往系统目录里面放东西。

这些都做好了你就可以试着编写一个使用SDL的Delphi程序了。我第一次做的时候是选了一个新建控制台程序,这样打开游戏的时候会有两个窗口。这倒不是问题,至少你可以随时把

{$APPTYPE CONSOLE}

这一行注释掉取消那个控制台窗口,但是我建议你还是暂时留着。至少控制台窗口的关闭按钮是随时都有效的,而SDL的主窗口的关闭按钮却不一定随时能用,在编写和调试阶段还是很方便的。

这时你有了一个dpr文件,如果把所有的函数与子程全写到这里当然不成问题,但是我认为更好的办法是把子程全写到另一个pas文件里,再在dpr文件的uses部分添加这个pas文件。因为在dpr文件里面我还没搞清楚能不能用函数预声明,如果不能的话就只能引用前面已经写好的部分,这无论如何不是个好的选择。而pas文件里面随意性就大得多。

这样你的dpr文件的内容大致应是这样(这里的语法加亮不行,凑合着看吧):

program MyGame

{$APPTYPE CONSOLE}

uses

  SysUtils,

  windows,

  Dialogs,

  SDL,

  myfunctions in 'myfunctions.pas';

begin

  Run;

end.

而myfunctions.pas文件里面应有这样的内容:

unit myfunctions;

interface

uses
  sysutils,
  windows,
  sdl;

procedure Run;

implementation

var
  screen: PSDL_Surface;

procedure Run;
begin
  SDL_Init(SDL_INIT_VIDEO);
  screen:=SDL_SetVideoMode(640,480,32,SDL_HWSURFACE or SDL_DOUBLEBUF);
  while true do
  ;

SDL_Quit;      
  exit;

end;

end.

uses里面加上Windows和Sysutils显然是必要的,因为我们可能还需要Windows的API。调试中如果想用showmessage显示一些结果的话还需要Dialogs,它比MessageBox还是方便很多。

全局变量里有一个screen,用它作为最主要的画图板,其余的画图板可以在使用的时候由子程定义。至于类型译成“SDL表面指针”看来也并不合适,不过没关系,知道它是做什么的就可以了。

SDL_Init(SDL_INIT_VIDEO)的作用是初始化视频系统,SDL_INIT_VIDEO其实是一个定义好的常数。一般这样用大致是可以,但是一个较为正式的用法应是这样:

if (SDL_Init(SDL_INIT_VIDEO)<0) then  
  begin      
    MessageBox(0, PChar(Format('Couldn''t initialize SDL : %s',[SDL_GetError])), 'Error', MB_OK or MB_ICONHAND);
    SDL_Quit;      
    exit;  
  end;

就是尝试初始化视频系统,一旦失败用MessageBox输出错误消息(SDL_GetError),并退出程序。不仅是视频,其他的许多比如初始化音频时都应该添加类似的判断。我在这里略去了这部分是为了让代码看来清晰一点,但是你如果真的动手编写一个游戏的时候,判断一下无疑还是必要的。

而在初始化视频之后,还需要一个窗口显示游戏画面,就是

screen:=SDL_SetVideoMode(640,480,32,SDL_HWSURFACE or SDL_DOUBLEBUF);

这一行的作用了,前面3个数字是屏幕的分辨率和色深,后面就是初始化的一些选项了。这里用的两个选项是在显存中生成画面并使用双缓冲,可能是要求比较高的一种,其实用 SDL_ANYFORMAT 和 SDL_SWSURFACE 也许就能满足多数情况。这里就不再一一解释这些选项的含义了,可以查看SDL的说明文件。

这样就获得了一个640*480大小的窗口,并用screen标记它。

再往后是一个死循环,这只是为了让这个窗口保持住,否则再执行下去,SDL_Quit 加上 exit 谁都知道代表什么含义。当然我们并非是真的需要一个死循环,一个游戏所需的循环应是这样:

1 如果接收到指令
2 执行指令
3 返回1

在执行指令这里应有:

如果指令为“退出”,则退出循环。

把原来的死循环这样改写,并在全局变量里面添加一个event:

event: TSDL_event;
  ……

while SDL_PollEvent(@event)>=0 do
  begin
    if event.type_=SDL_QUITEV then break;
  end;

event是定义用来接收游戏的消息(事件)的,任何游戏都应该有这样的一个变量(而且似乎只能作为全局变量或者在最初的子程序中定义,这一点我还并没确定)。而PollEvent就是用于查询的,@在Delphi中是取地址算符,因为SDL_PollEvent的参数是指针类型。

通常可以用两种方式进行查询:

SDL_PollEvent:直接查询。返回值为0表示未查询到事件,大于0表示有事件。无论查询结果如何,程序都会继续执行。

SDL_WaitEvent:等待查询。与上面的区别是如果未查询到事件,会使游戏的流程停在此句。

何时使用这两种方式需根据情况决定。如果你觉得某些情况只需处理按键(比如在选单内部),那么Wait方式会好一些;而如果在没有事件的时候你需要一些自动效果,Poll方式也许更适合。

循环中的判断就是:一旦发生关闭窗口事件(SDL_QUITEV,注意在说明文档里面写成了SDL_QUIT,这与SDL中用于退出的重要函数同名,显然是写错了),则退出循环。

这个程序在执行时可能会占用CPU过大,这时可以在每次循环时让CPU休息一下。在while内
部加上一个:

Sdl_delay(10);

即可大大降低CPU的占用,这可能会带来10毫秒的延时,但不会真的有人在乎吧!

SDL & Object Pascal (Delphi) [前言]

这个系列最早发布在blogspot上,我一直以为已经转过来了,今天发现并没有。

那就转一次吧,现在blogspot基本上已经不再管了。

实际上这个名字叫[SDL & Delphi]也不成问题, 因为除了Delphi似乎也没有哪个流行的开发工具用的是Pascal语言。

SDL其实我也只学了不到两星期而已。刚开始我想试图用VC,因为这样资料最全,也很好找,但太久没用VC现在看C的代码有点困难(->这个算符是干吗的来着……)。其实作为一个专业不是计算机的人,平时写一些程序都是用Delphi,这样我不必在界面上费劲,至于VC虽然用过但扔了很久了。后来我发现SDL有Object Pascal版本,就安装了一个,觉得用起来还是很方便的。

当然我说过我只是个业余的,所以我使用的一些代码可能不太正规,至少变量的匈牙利命名方法我是很少用的,只是我嫌打字费劲。还有我用的一些实现方法也许有更高效的方案。但我还是想把一些使用上的经验和走过的一些弯路写在这里,大家参考也好,找找毛病也好,我都是欢迎的。