lw 发表于 2006-11-4 14:32:04

[第二部分]为状态写代码

为状态写代码
(前言:不是什么新东西,不过我自己还没有听到过类似的概念,所以就自己先来个写写)

游戏好比一个具有状态的机器(不要理解为状态机,那是复杂的数据结构的陷阱),我们在决定了草案(请参考第一部分关于设计的一丁点内容)以后往往不知道怎么搭起框架,本部分希望能够在AC开发的设计理念基础上为大家准备好一个通用的框架,而基础的核心,就是为状态写代码

通常游戏的循环部分(我省略了加载和卸载部分内容)可以更细地划分为如下一些部分:(以AC为例,如果
不知道AC这个是什么,可以到我自己的GOOGLEPAGE上先看看)
游戏被划分为很多步骤(STEP):

    enum STEP {
      STEP_STARTANIM,         
      STEP_CREATE,            
      STEP_DELAY,            
      STEP_MOVEDOWN,         
      STEP_AUTODOWN,         
      STEP_CHECK,            
      STEP_CHECKEFFECT,         
      STEP_ATTACK,            
      STEP_ATTACKEFFECT,         
      STEP_DEFENCE,         
      STEP_ENDANIM,         
      STEP_EXIT,            

      STEP_COUNT,            
    } ;

我很乐意给每个步骤加上注释,但是“代码与注释脱离”告诉我们应该避免让具名的标签加上注释
(AVC很喜欢不写注释,但是他的变量你很少看到X1 Y3这类的名字),每个单位表示一个具体的流程(就是状态名),最后COUNT表示总计个数,然后你:

typedef void (Player::*Flow)( void ) ;
Flow flowfunction ;

现在所有的步骤就是索引了(这里有一个小小问题,STEP和INDEX可能不兼容,为了方便你就STATIC_CAST好了,嗯……不知道什么是STATIC_CAST的人……你干脆想个别的办法,代替STEP名称巴……)当你创建每个步骤的时候,你所做的事情就是在 进行状态分离 (当然你根据你自己划分,尽可能细,简单的划分就是你游戏的处理环节,你这样想,游戏会不断重复这个步骤,是不是每次重复的内容都类似?举个例子:AC的小球下落被进行状态分离,首先小球会被创建i.e.CREATE,然后小球会先在高处停留一会,就是DELAY,接着开始下落MOVE,就这样分成了三步,然后你想想每次会执行一个微小的步骤,唉你学过高等数学的微积分多好,DELTA X dx对,你会很高兴发现你原来已经知道了游戏是怎么运作了,你的思路会从整体中被分离开,每次一个dx^^)

一大堆,实际就是一个enum和一个function然后怎么运作这个函数的呢?

    flowfunction = &Player::startAnim;
    flowfunction = &Player::createBall;
    flowfunction = &Player::delay;
    flowfunction = &Player::moveDown;
    flowfunction = &Player::autoDown;
    flowfunction = &Player::check;
    flowfunction = &Player::checkeffect;
    flowfunction = &Player::attack;
    flowfunction = &Player::attackeffect;
    flowfunction = &Player::defence;
    flowfunction = &Player::endAnim;
    flowfunction = &Player::exitGame;

这么多!oh no!就一句话罢了……让你的每个player的函数注册到你步骤中去……看到了吗?为了方便,所有的函数就是对应步骤的名称的去STEP+小写罢了,那么这些事情是什么时候做得?
Player::Player(),构造函数……不懂的话……那你就放在init过程里面好了

嗯,现在是回顾一下的时候了,你在Manager部分(参考第一部分)创建了Player,Player构造的时候呢,注册了步骤对应的函数,然后你就不用关心流程怎么做了,你记住,现在你的函数的调用被简化到了你的一个标记上

UINT_PTR attack_flag;

UINT_PTR是什么,你大叫……嗯,让我解释不如让你自己换一个好了unsigned int,嗯,你点头,我可以用int吗(你说:我比较懒- -)完全可以^^,不过不要有负数^^

第一个部分中,我写了我是用一个MOVE一个RENDER实现的代码(没说清,就是Player::Move(), Player::Render() ),所以这里我们看看MOVE实作手法:

void Player::move( void )
{
    (this->*flowfunction)() ;
}

哦……就一句话,看不懂吗- - ,反正就是这样调用注册版函数……

偓哦!原来你要这么做:
Manager::Loop --> 其中调用每个一个Player对象的 Player::Move() -->
后者调用flowfunction,根据当前的attackflag调用现在的函数,这叫一帧Frame,然后你那个函数作了什么(那我不知道了,反正你游戏作啥?偶很乐意知道哦^^),然后每次重复,所以要LOOP!

一个要求,flowfunction需要知道什么时候,怎么样改变状态?那是,比如AC中时间一到,下落,letschange state!

++attack_flag; return;

就这样,真的,我觉得编写的生活突然美好了很多,不需要再去调用具体的函数,反正下一次就是另外的状态了……(就是DELAY状态对应的函数被调用了)
C语言代码写作的人很喜欢用小函数,我从她们那里偷了这个思路^^,暂时就写这么点,希望大家能够从这么点东西中受到启发,而且我很乐意和大家讨论,中间也有很多不足,很希望高手指点,谢谢!

参考书籍:《WINDOWS游戏编程大师技巧》

shawind 发表于 2006-11-5 16:07:36

看了三遍了,老实说,完全没有看懂...
强烈要求放出直观的流程图

Yuki 发表于 2006-11-5 17:17:51

引用第1楼shawind于2006-11-05 16:07发表的:
看了三遍了,老实说,完全没有看懂...
强烈要求放出直观的流程图
一句话.函数指针数组,每一个状态都对应数组内的一个函数指针. c style
还有当一个状态可以转向好几个状态时或者转上上面的状态.++flag就不行了.要直接flag = .....还是一样.

shawind 发表于 2006-11-5 17:33:53

哦,原来是这样。
一直在想00方面的事情,没想到这是c风格的。
这么一说就明白了。

lw 发表于 2006-11-5 18:38:27

我想尽量把OO说进去,但是无奈选材不好……唉……

话说怎么样是OO风格啊……

看来我的理解不对,一直以为对象拥有状态,就是一种OO设计了……原来是C风格的,我不知道,那么怎么样才是OO风格啊?一定要有VIRTUAL吗?


还有当一个状态可以转向好几个状态时或者转上上面的状态.++flag就不行了.要直接flag = .....还是一样

从代码上来看一样,实际上理解会不一样,因为流程应该是顺序的,显式调用flag 说明你在对流程不断进行操作,说明本身的流程是不清晰的,当然某些操作必须显式进行切换状态(比如死亡、逆转返回等情况)你这个时候就应该特别关注……等等,我想………………

Advance 发表于 2006-11-5 20:03:53

为什么会提到我不喜欢写注释的啊~_~
我通常用一个仅包含static const的interface而不是enum来存放状态和标志,然后一些需要使用const的class就可以继承自这个interface,这样让程序明确不去引用无关的常量,似乎也不错

shawind 发表于 2006-11-6 11:25:04

oo风格的,这个我哪能说清楚。
感觉应该是像krkr2的kag引擎那样的写法吧。
当然那种思路的运行效率是不如这样的来得快的。

Yuki 发表于 2006-11-6 18:51:13

其实我说这个是C STYLE 的原因是方便大家理解的.c style 本来就不是坏事.这种少数状态的状态机就应该用C STYLE 写.又方便又清楚.用OO 反而麻烦了很多.例如用state模式.每一个state 都要派生出一个子类.当state多的时候就会出现类爆炸.还有一种方法是适用于大量状态或状态图变化频繁的状态机.我说一下这个方法.其实也是基于c style 的思路.但是效率比较低.

//CState.h
#ifndef YUKI_CSTATE_H
#define YUKI_CSTATE_H

#include <string>

class CStateMap;

typedef std::string STATE_ID;
typedef STATE_ID (*STATE_FUNC) (void * params) ;

class CState
{
public:
    STATE_ID executeState(void * params){
      return stateFunc(params);
    }
protected:
    friend class CStateMap;
    CState(STATE_FUNC _stateFunc)
      :stateFunc(_stateFunc){}
private:
    STATE_FUNC stateFunc;
};

#endif

// CStateMap.h
#ifndef YUKI_CSTATEMAP_H
#define YUKI_CSTATEMAP_H

#include <map>
#include "CState.h"

class CStateMap
{
public:
    bool setStartState(STATE_ID id);
    bool registerState(STATE_ID id, STATE_FUNC func);
    bool execute(void * params);
    STATE_ID getNowState();
private:
    typedef std::map<STATE_ID,CState> STATE_MAP;
    STATE_MAP stateMap;
    STATE_ID nowState;
};

#endif
//CStateMap.cpp
#pragma warning(disable:4786)

#include "CStateMap.h"

bool CStateMap::setStartState(STATE_ID id)
{
    if (stateMap.find(id) != stateMap.end())
    {
      nowState = id;
      return true;
    }
    return false;
}

bool CStateMap::registerState(STATE_ID id, STATE_FUNC func)
{
    std::pair<STATE_MAP::iterator,bool> ret;
    ret = stateMap.insert( STATE_MAP::value_type( id,CState(func) ) );
    return ret.second;
}

bool CStateMap::execute(void * params)
{
    STATE_MAP::iterator state_it = stateMap.find(nowState);
    if (state_it != stateMap.end())
    {
      nowState = (*state_it).second.executeState(params);
      return true;
    }
    return false;
}

STATE_ID CStateMap::getNowState()
{
    return nowState;
}
//main.cpp
#pragma warning(disable:4786)

#include <stdio.h>
#include "CStateMap.h"

STATE_ID Start(void * params)
{
    printf ("Start !! \\n");
    return "Run";
}
STATE_ID Run(void * params)
{
    static int i = 0;
    i ++;
    if (i >= 24)
      return "Exit";
    if (i % 3 == 0)
      return "Pause";   
    char * pChar = (char *)params;
    printf (pChar);   
    return "Run";
}
STATE_ID Pause(void * params)
{
    printf("Pause!! \\n");
    return "Run";
}
STATE_ID Exit(void * params)
{
    printf("Exit !!\\n");
    return "";
}
int main ()
{
    CStateMap c;
    c.registerState("Start",Start);
    c.registerState("Run",Run);
    c.registerState("Pause",Pause);
    c.registerState("Exit",Exit);
    c.setStartState("Start");
    while (true)
    {
      bool running ;
      if (c.getNowState() == "Run")
            running = c.execute("Run!!!\\n");
      else running = c.execute(0);
      if (running == false)
            break;
    }
    return 0;
}


其实就是用一个函数指针map.和cstyle差不多.就是添加状态比较方便而已...游戏的状态机还是用c style好.

lw 发表于 2006-11-6 19:50:00

引用第5楼Advance于2006-11-05 20:03发表的:
为什么会提到我不喜欢写注释的啊~_~

上班了才知道的,不是什么注释都应该写,我的意思是说,AVC比较清楚什么时候更应该用确切的VARIABLE NAME 代替无聊的注释……有时候注释反而是绊脚石啊……


我通常用一个仅包含static const的interface而不是enum来存放状态和标志,然后一些需要使用const的class就可以继承自这个interface,这样让程序明确不去引用无关的常量,似乎也不错
enum主要是可以不关心确切的数值,而且看起来比较清晰,

其实一开始3个状态的时候偶确实是STATIC CONST INT 的。。。

Advance 发表于 2006-11-7 00:06:34

flowfunction = &Player::startAnim;
flowfunction = &Player::createBall;
flowfunction = &Player::delay;


那你的代码就不能像上面这样写了,没有为enum成员指定明确值的时候最好不要将它转换成任何数据类型
页: [1] 2
查看完整版本: [第二部分]为状态写代码