九、编写图片益智游戏

在本章中,我们将介绍:

  • 实现图片益智游戏逻辑
  • 实现动画 3D 图像选择器
  • 基于页面的用户界面
  • 带有 Picasa 下载器的图像库
  • 实现完整的拼图游戏

简介

在这一章中,我们继续整理前几章中的食谱。我们将实现一个图片拼图游戏,玩家需要把拼图拼在一起,以重建原始图像。图像从 Picasa 照片主机的特色图库中流出,并可通过 3D 动画图像选择器选取。我们的游戏有一个简单的基于页面的用户界面,可以作为一个更复杂的游戏用户界面框架的起点。

本章的示例项目实际上是作者在 Google Play 上发布的 Linderdaum 拼图高清游戏的简化版:http://play.google.com/store/apps/details?id = com . linder daum . engine . puzzlhd

实现图片益智游戏逻辑

这个食谱向你展示了如何实现图片益智游戏的游戏逻辑。游戏由一组在屏幕上洗牌和渲染的矩形方块组成。用户可以点击单个图块,并移动它们,将它们与其他图块交换。让我们起草主干数据结构来实现这个逻辑。

做好准备

要更好的感受游戏逻辑,可以搭建运行2_PuzzleProto项目,可以从www.packtpub.com/support下载。如果你想享受全功能游戏,就去从 GooglePlay 下载我们的林道拼图高清版。你可以在http://play.google.com/store/apps/details?做 id = com . linder daum . engine . puzzlhd

Getting ready

怎么做...

  1. 首先,我们需要clTile类来存储单个拼图块的状态。它包含图块左上角的当前坐标、栅格中图块的原始索引以及此图块将移动到的目标坐标:

    cpp class clTile { public: int FOriginX, FOriginY; vec2 FCur, FTarget; LRect FRect; clTile(): FOriginX( 0 ), FOriginY( 0 ) {};

  2. 第二个构造函数计算并设置FRect字段,该字段包含稍后用于渲染的纹理坐标:

    cpp clTile( int OriginX, int OriginY, int Columns, int Rows ): FOriginX( OriginX ) , FOriginY( OriginY ) {

  3. 计算图块的纹理坐标并存储在FRect :

    cpp float TileWf = 1.0f / Columns, TileHf = 1.0f / Rows; float X1f = TileWf * ( OriginX + 0 ); float X2f = TileWf * ( OriginX + 1 ); float Y1f = TileHf * ( OriginY + 0 ); float Y2f = TileHf * ( OriginY + 1 ); FRect = LRect( X1f, Y1f, X2f, Y2f ); FTarget = FCur = vec2( OriginX, OriginY ); }

    中 4. 接下来的两种方法设置目标和当前坐标:

    cpp void SetTarget( int X, int Y ) { FTarget = vec2( X, Y ); } void MoveTo( float X, float Y ) { FCur.x = X; FCur.y = Y; };

  4. 图块平滑地移动到目标坐标。我们使用时间计数器更新图块位置,对于每个时间步长,坐标被重新计算:

    cpp void Update( float dT ) { vec2 dS = FTarget - FCur; const float c_Epsilon = 0.001f; if ( fabs( dS.x ) < c_Epsilon ) { dS.x = 0; FCur.x = FTarget.x; } if ( fabs( dS.y ) < c_Epsilon ) { dS.y = 0; FCur.y = FTarget.y; } const float Speed = 10.0f; FCur += Speed * dT * dS; } };

  5. 游戏的状态由一组牌表示,这些牌存储在clPuzzle类中:

    ```cpp class clPuzzle { public: mutable std::vector FTiles; int FColumns, FRows; bool FMovingImage; int FClickedI, FClickedJ; float FOfsX, FOfsY;

    clPuzzle()
    FMovingImage( false ) , FClickedI( -1 ), FClickedJ( -1 ) , FOfsX( 0.0f ), FOfsY( 0.0f ) { Retoss( 4, 4 ); } ... ```
  6. 交换两块由它们的(i,j) 2D 坐标指定的瓷砖:

    cpp void SwapTiles( int i1, int j1, int i2, int j2 ) { std::swap( FTiles[j1 * FColumns + i1],FTiles[j2 * FColumns + i2] ); } };

  7. 如果所有的牌都就位,游戏就完成了。要检查瓷砖是否到位,我们需要将其FOriginXFOriginY坐标与当前的ij坐标进行比较:

    cpp bool clPuzzle::IsComplete() const { for ( int i = 0; i != FColumns; i++ ) { for ( int j = 0; j != FRows; j++ ) { clTile* T = GetTile( i, j ); if ( T->FOriginX != i || T->FOriginY != j) return false; } } return true; }

  8. clPuzzle::Timer()调用Update()方法,该方法为每个图块计算新坐标。这是需要的,以便玩家释放接触后,瓷砖能够回到它们的位置:

    cpp void clPuzzle::Timer( float DeltaSeconds ) { for ( int i = 0; i != FColumns; i++ ) { for ( int j = 0; j != FRows; j++ ) GetTile( i, j )->Update( DeltaSeconds ); } }

  9. 游戏的初始状态在Retoss()方法中生成:

    cpp void Puzzle::Retoss(int W, int H) { FColumns = W; FRows = H; FTiles.resize( FColumns * FRows );

  10. 首先,我们在所有瓷砖的初始位置创建它们:

    cpp for ( int i = 0; i != FColumns; i++ ) for ( int j = 0; j != FRows; j++ ) FTiles[j * FColumns + i] =clTile( i, FRows - j - 1, FColumns, FRows );

  11. 然后,我们使用 Knuth shuffle,也称为 Fisher–Yates shuffle(http://en.wikipedia.org/wiki/Fisher–Yates_shuffle)来生成瓷砖的随机排列:

    cpp for ( int i = 0; i != FColumns; i++ ) { for ( int j = 0; j != FRows; j++ ) { int NewI = Math::RandomInRange( i, FColumns - 1 ); int NewJ = Math::RandomInRange( j, FRows - 1 ); SwapTiles( i, j, NewI, NewJ ); } } … }

  12. 用户输入的处理在OnKey()方法中执行。当用户在屏幕上按下鼠标按钮或点击时,调用该方法时KeyState参数等于真。在鼠标释放或轻击结束时,调用OnKey()方法,并将KeyState设置为假。mxmy参数包含触摸的 2D 坐标。一旦触摸被激活,我们存储图块的索引和触摸点相对于图块左上角的初始偏移:

    ```cpp void Puzzle::OnKey( float mx, float my, bool KeyState ) { int i = (int)floor( mx * FColumns ); int j = (int)floor( my * FRows ); int MouseI = ( i >= 0 && i < FColumns ) ? i : -1; int MouseJ = ( j >= 0 && j < FRows ) ? j : -1; FMovingImage = KeyState; if ( FMovingImage ) { FClickedI = MouseI; FClickedJ = MouseJ;

    if ( FClickedI >= 0&& FClickedJ >= 0&& FClickedI < FColumns&& FClickedJ < FRows )
    {
      FOfsX = ( ( float )FClickedI / FColumns - mx );
      FOfsY = ( ( float )FClickedJ / FRows    - my );
    }
    else
    {
      FClickedI = FClickedJ = -1;
    }
    

    } else ```

  13. 当触摸结束时,我们检查新平铺位置的有效性,并将所选平铺与新位置的平铺进行交换:

    cpp { bool NewPosition = ( MouseI != FClickedI ||MouseJ != FClickedJ ); bool ValidPosition1 = ( FClickedI >= 0 && FClickedJ >=0 && FClickedI < FColumns && FClickedJ < FRows ); bool ValidPosition2 = ( MouseI >= 0 && MouseJ >= 0 &&MouseI < FColumns && MouseJ < FRows ); if ( NewPosition && ValidPosition1 && ValidPosition2 ) { int dX = MouseI - FClickedI; int dY = MouseJ - FClickedJ; SwapTiles( FClickedI, FClickedJ, MouseI, MouseJ ); } if ( IsComplete() ) { // TODO: We've got a winner! } FClickedI = FClickedJ = -1; } }

它是如何工作的...

2_PuzzleProto示例使用clPuzzle类来显示没有任何纹理或任何花哨图形的游戏性。

要渲染拼图的状态,使用以下程序:

void RenderGame( clPuzzle* g, const vec4& Color )
{

如果我们选择了图块,我们会将其移动到新的鼠标或触摸位置:

  if ( g->FMovingImage && g->FClickedI >= 0 &&g->FClickedI >= 0 &&g->FClickedI < g->FColumns &&g->FClickedJ < g->FRows )
  {
    vec2 MCI = Env_GetMouse();
    int NewI = g->FClickedI;
    int NewJ = g->FClickedJ;
    float PosX, PosY;
    PosX = Math::Clamp( MCI.x + g->FOfsX, 0.0f, 1.0f );
    PosX *= g->FColumns;
    PosY = Math::Clamp( MCI.y + g->FOfsY, 0.0f, 1.0f );
    PosY *= g->FRows;
    g->GetTile( NewI, NewJ )->MoveTo( PosX, PosY );
  }

最后,使用DrawTile()方法调用渲染每个图块:

  for ( int i = 0; i != g->FColumns; i++ )
    for ( int j = 0; j != g->FRows; j++ )
      DrawTile( g, i, j, Color );
}

DrawTile()方法计算图块在归一化屏幕坐标(0...1)中的坐标,并使用矩形顶点阵列和g_Canvas对象渲染Tile实例:

void DrawTile( clPuzzle* g, int i, int j, const vec4& Color )
{
  if ( i < 0 || j < 0 || i >= g->FColumns || j >= g->FRows )
  { return; }
  clTile* Tile = g->GetTile( i, j );
  Tile->SetTarget( i, j );
  float X = Tile->FCur.x;
  float Y = Tile->FCur.y;
  float TW = 1.0f / g->FColumns;
  float TH = 1.0f / g->FRows;
  vec4 TilePosition(TW * ( X + 0 ), TH * ( Y + 0 ),TW * ( X + 1 ), TH * ( Y + 1 ) );
  g_Canvas->TexturedRectTiled(TilePosition, 1.0f, 1.0f, g_Texture,Effect, Color, VA, Tile->GetRect() );
}

在下一个食谱中,我们将这个简单的游戏与动画图像选择器和 Picasa 图像下载器相结合,创建了一个功能更丰富的益智游戏。

实现动画三维图像选择器

我们的益智游戏的主要 UI 元素是动画 3D 图像选择器。在这个食谱中,我们向您展示了如何渲染动画旋转木马状的选择器并与用户交互。

做好准备

在继续这个食谱之前,你可能需要回到第 7 章跨平台 UI 和输入系统,阅读Canvas类的工作原理。需要一些数学知识来更好地理解这个食谱中的代码是如何工作的。

怎么做...

渲染背后的想法很简单。我们让单个四边形以它们的角沿着四条引导曲线滑动的方式移动。下图显示了处于一系列位置的同一个四边形:

How to do it...

四条曲线显示了四边形拐角的路径。

  1. We start with the helper Curve class, which implements the linear interpolation on the set of control points. A curve is represented in a parametric form.

    曲线的参数方程是通过将曲线各点的坐标表示为称为参数的变量的函数的方程来表示该曲线。

    礼遇:http://en.wikipedia.org/wiki/Parametric_equation

    cpp class Curve { public: Curve() {}

  2. AddControlPoint()方法向曲线添加一个新的控制点。曲线是延迟评估的,现在我们只存储指定的值:

    cpp void AddControlPoint( float t, const vec3& Pos ) { T.push_back( t ); P.push_back( Pos ); }

  3. GetPosition()方法为给定参数t找到一个线段,并计算曲线上一个点的线性插值坐标:

    cpp vec3 GetPosition( float t ) const { if ( t <= T[0] ) { return P[0]; } int N = (int)T.size(); int i = N - 1; for ( int s = 0 ; s < N - 1 ; s++ ) { if ( t > T[s] && t <= T[s + 1] ) { i = s; break; } } if ( i >= N - 1 ) { return P[N - 1]; } vec3 k = ( P[i + 1] - P[i] ) / ( T[i + 1] - T[i] ); return k * ( t - T[i] ) + P[i]; }

  4. 控制点和相应的参数存储在两个向量中:

    cpp std::vector<float> T; std::vector<vec3> P; };

  5. 3D 图像选择器控制逻辑在clFlowUI类中实现:

    cpp class clFlowUI: public iObject { public: clFlowUI( clPtr<clFlowFlinger> Flinger, int NumQuads ) { FFlinger = Flinger;

  6. 为我们的用户界面创建一个 3D 相机:

    cpp mtx4 RotationMatrix; RotationMatrix.FromPitchPanRoll( 0.0f, -90.0f, 0.0f ); FView = mtx4::GetTranslateMatrix(-vec3( 0.0f, -13.2f, 1.2f ) ) * RotationMatrix;

  7. 使用标准透视相机:

    cpp FProjection = Math::Perspective(45.0f, 1.33333f, 0.4f, 2000.0f ); float Y[] = { c_Height, c_Height, 0, 0 }; float Offs[] = { -c_PeakOffset, c_PeakOffset,c_PeakOffset, - c_PeakOffset }; float Coeff[] ={ c_Slope, - c_Slope, - c_Slope, c_Slope }; for ( int i = 0 ; i < 4 ; i++ ) { const int c_NumPoints = 100; for ( int j = - c_NumPoints / 2 ;j < c_NumPoints / 2 + 1 ; j++ ) { float t = ( float )j * c_PointStep; float P = Coef[i] * ( Ofs[i] - t );

  8. 反正切相乘by exp(-x^2) :

    cpp float Mult = c_FlowMult *exp( - c_FlowExp * P * P ); vec3 Pt( -t, Mult * c_Elevation *atan( P ) / M_PI, Y[i] ); FBaseCurve[i].AddControlPoint(t *exp( c_ControlExp * t * t ), Pt); } } …

  9. 使用当前元素数量更新用户界面滚动限制:

    cpp FFlinger->FMinValue = 0.0f; FFlinger->FMaxValue = c_OneImageSize *( ( float )FNumImg - 1.0f ); }

  10. 计算当前选中索引图像的索引:

    cpp int GetCurrentImage() const { return (int)ceilf( FFlinger->GetValue() / OneImageSize ); }

  11. 单个四边形的坐标通过QuadCoords()方法计算,其中为四条引导曲线中的每一条调用【T1:

    cpp virtual void QuadCoords( vec3* Pts, float t_center )const { float Offs[] ={ c_QuadSize, - c_QuadSize, - c_QuadSize, c_QuadSize }; for ( int i = 0 ; i < 4 ; i++ ) Pts[i] = FBaseCurve[i].GetPosition(t_center - Offs[i] / 2 ); }

  12. 添加每条基础曲线的轨迹控制点:

    cpp Curve FBaseCurve[4]; };

  13. 以下是引导曲线的参数。顺序控制点之间的屏幕单位数(以归一化坐标表示):

    cpp const float c_PointStep = 0.2f;

  14. 四点的经验调整参数,速度:

    cpp const float c_ControlExp = 0.001f;

  15. 图像的高度,即上下曲线之间的距离、厚度和曲线的斜率:

    cpp const float c_Height = 4.0f const float c_Elevation = 2.0f; const float c_Slope = 0.3f;

  16. 曲线峰值、指数衰减和主系数的对称位移:

    cpp const float c_PeakOffset = 3.0f; const float c_FlowExp = 0.01f; const float c_FlowMult = 4.0f;

  17. clFlowFlinger类保存选择器的动态状态:

    ```cpp
    class clFlowFlinger: public iObject
    {
    public:
    clFlowFlinger()
    FPressed( false ), FValue( 0.0f ), FVelocity( 0.0f ) {} virtual ~clFlowFlinger() {} ```
  18. 决定对选择做什么—如果选择完成,返回true,否则返回false:

    cpp virtual bool HandleSelection( float mx, float my ){ return false; }

  19. 更新动画和手柄触摸:

    cpp void Update( float DeltaTime ); void OnTouch( bool KeyState ); … };

  20. 触摸处理以OnTouch()方式进行:

    cpp void clFlowFlinger::OnTouch( bool KeyState ) { int CurImg = ( int )ceilf( FValue / OneImageSize ); vec2 MousePt = Env_GetMouse(); double MouseTime = Env_GetMouseTime(); FPressed = KeyState; if ( KeyState ) { FClickPoint = FLastPoint = MousePt; FClickedTime = FLastTime = MouseTime; FInitVal = FValue; FVelocity = 0; } else {

  21. 如果触摸点移动了不到 1%的屏幕,或者手势用了不到 10 毫秒,我们认为这是一次点击:

    cpp double Time = MouseTime - FClickedTime; double c_TimeThreshold = 0.15; float c_LenThreshold = 0.01f; if ( ( FClickPoint - MousePt ).Length() <c_LenThreshold&& ( Time < c_TimeThreshold ) ) { HandleSelection( MousePt.x, MousePt.y ); FVelocity = 0; return; }

  22. 否则,如果手势跨度小于 300 毫秒,我们停止运动:

    cpp float c_SpanThreshold = 0.3f; float dT = (float)( MouseTime - FLastTime ); float dSx = MousePt.x - FLastPoint.x; FVelocity = ( dT < c_SpanThreshold ) ?-AccelCoeff * dSx / dT : 0; } }

  23. 位置和时间的系数是根据对运动的感知凭经验选择的。动态在Update()方法中实现:

    cpp void clFlowFlinger::Update( float DeltaTime ) { float NewVal = 0.0f; if ( FPressed ) { vec2 CurPoint = Env_GetMouse(); NewVal = FInitVal; NewVal -= AccelCoef * ( CurPoint.x - FLastPoint.x ); } else { NewVal = FValue + FVelocity * DeltaTime; FVelocity -= FVelocity * c_Damping * DeltaTime;

  24. 当我们到达最后一个图像时,我们只需在引导曲线上夹紧位置。为了获得流畅的体验,我们还添加了一个橡皮筋效果,使用线性公式对位置进行插值。Damper系数纯粹是经验性的:

    cpp const float Damper = 4.5f * DeltaTime; if ( NewVal > FMaxValue ) { FVelocity = 0; NewVal = FMaxValue * Damper + NewVal * ( 1.0f - Damper ); } else if ( NewVal < FMinValue ) { FVelocity = 0; NewVal = FMinValue * Damper +NewVal * ( 1.0f - Damper ); } } FValue = NewVal; }

  25. FlowFlinger.h文件

    cpp const float c_AccelCoeff = 15.0f; const float c_ValueGain = 0.1f; const float c_IntGain = 0.1f; const float c_DiffGain = 0.1f; const float c_Damping = 0.7f;

    中定义了一组舒适滚动的参数

鼓励你尝试自己的价值观。

它是如何工作的...

旋转木马渲染基于Canvas并在RenderDirect()功能中实现:

void RenderDirect( clPtr<clFlowFlinger> Control )
{
  int Num = Control->FNumImg;
  if ( Num < 1 ) { return; }
  int CurImg = Control->GetCurrentImage();
  float Dist = ( float )( Num * c_OneImageSize );

我们手动指定四元渲染顺序。首先,我们渲染左侧图像,然后渲染右侧图像,最后渲染中心图像:

  int ImgOrder[] = {CurImg - 3, CurImg - 2, CurImg - 1,CurImg + 3, CurImg + 2, CurImg + 1,CurImg };

阵列边界检查的实际渲染,以及将ProjectionView矩阵应用到四边形的每个角:

  for ( int in_i = 0 ; in_i < 7 ; in_i++ )
  {
    int i = ImgOrder[in_i];
    if ( i < 0 )
      { i += ( 1 - ( ( int )( i / Num ) ) ) * Num; }
    if ( i >= Num )
      { i -= ( ( int )( i / Num ) ) * Num; }
    if ( i < Num && i > -1 )
    {
      vec3 Pt[4];
      Control->QuadCoords(Pt,Control->FFlinger->FValue - ( float )(i) *c_OneImageSize);
      vec3 Q[4];
      for(int j = 0 ; j < 4 ; j++)
        Q[j] = Control->FProjection *Control->FView * Pt[j];
      BoxR(Q, 0xFFFFFF);
    }
  }
}

最终的渲染使用BoxR()函数完成,该函数在main.cpp文件中实现。

需要对转盘代码进行修改以支持选择。我们在GeomUtil.h文件中加入了一些相交测试的方法。类似于RenderFlow()过程,我们在可见图像上迭代,对于其中的每一个,我们通过图像平面与来自抽头位置的光线相交:

int clFlowUI::GetImageUnderCursor( float mx, float my ) const
{
  if ( FNumImg < 1 ) { return -1; }

将 2D 屏幕触摸点映射到三维点和光线:

  vec3 Pt, Dir;
  MouseCoordsToWorldPointAndRay( FProjection, FView,mx, my, Pt, Dir );
  int CurImg = GetCurrentImage();
  int ImgOrder[] = { CurImg, CurImg - 1, CurImg + 1, CurImg - 2,CurImg + 2, CurImg - 3, CurImg + 3 };

迭代当前图像四边形:

  for ( int cnt = 0 ; cnt < countof( ImgOrder ) ; cnt++ )
  {
    int i = ImgOrder[cnt];
    if ( i < 0 || i >= (int)FNumImg ) { continue; }

将四边形坐标转换到世界空间:

    vec3 Coords[4];
    QuadCoords( Coords, FFlinger->GetValue() ( float )(i) * OneImageSize );

将光线与两个三角形相交:

    vec3 ISect;
    if ( IntersectRayToTriangle( Pt, Dir,Coords[0], Coords[1], Coords[2], ISect ) ||( IntersectRayToTriangle( Pt, Dir,Coords[0], Coords[2], Coords[3], ISect ) ) )
      return i;
  }
  return -1;
}

Unproject()MouseCoordsToWorldPointAndRay()功能将 2D 屏幕点坐标转换为三维世界空间中的光线,我们的旋转木马四边形在该空间中飞行。他们的实现可以在GeomUtil.h文件中找到。

要将选择器倒回某个特定图像,我们设置一个目标位置:

void SetCurrentImageTarget( int i )
{ FFlinger->SetTargetValue( ( float )i * ( OneImageSize ) ); }

还有更多...

在这个食谱中,我们使用三维线来渲染旋转木马。使用Canvas类来渲染每个带有纹理的四边形非常简单。我们还鼓励读者添加一个反射效果,这很容易通过渲染相同的四边形集和表示水平面反射的附加变换来完成。

另见

  • 实现完整的图片益智游戏

基于页面的用户界面

在前一章中,我们开发了一个包含单页的游戏。然而,大多数现代移动游戏都包含由复杂业务逻辑支持的复杂用户界面。典型的用户界面由几个带有多个用户界面元素的全屏页面组成,如按钮、图像和输入框。这些是使用游戏内渲染系统渲染的,不依赖于底层操作系统的用户界面。在这个食谱中,我们向你展示了如何解决这个问题。

做好准备

你可能想知道有哪些开源的 C++多平台用户界面库。以下链接将帮助您:http://en . Wikipedia . org/wiki/List _ of _ platform-independent _ GUI _ libraries

如果你想为你的游戏寻找一个全面的 HTML/CSS 用户界面(http://librocket.com),我们也推荐你去看看 libRocket 。它的集成很简单,但是超出了本书的范围。

怎么做...

  1. 单个页面处理所有按键、触摸、定时器和渲染事件:

    ```cpp class clGUIPage: public iObject { public: clGUIPage(): FFallbackPage( NULL ) {} virtual ~clGUIPage() {}

    virtual void Update(float DeltaTime) {} virtual void Render() {} virtual void SetActive(); ```

  2. 处理基本 UI 交互事件:

    cpp virtual bool OnKey( int Key, bool KeyState ); virtual void OnTouch( const LVector2& Pos, boolTouchState );

  3. 当点击后退电子稳定控制按钮时,我们返回到的页面:

    cpp clPtr<clGUIPage> FFallbackPage; … };

  4. 所有 UI 页面都由clGUI类管理,该类主要将所有事件委托给当前选择的页面:

    cpp class clGUI: public iObject { public: clGUI(): FActivePage( NULL ), FPages() {} virtual ~clGUI() {} void AddPage(const clPtr<clGUIPage>& P) { P->FGUI = this; FPages.push_back(P); } void SetActivePage( const clPtr<clGUIPage>& Page ) { if ( Page == FActivePage ) { return; } FActivePage = Page; } void Update( float DeltaTime ) { if ( FActivePage ) FActivePage->Update( DeltaTime ); } void Render() { if ( FActivePage ) FActivePage->Render(); } void OnKey( vec2 MousePos, int Key, bool KeyState ) { FMousePosition = MousePos; if ( FActivePage ) FActivePage->OnKey( Key, KeyState ); } void OnTouch( const LVector2& Pos, bool TouchState ) { if ( FActivePage )FActivePage->OnTouch( Pos, TouchState ); } private: vec2 FMousePosition; clPtr<clGUIPage> FActivePage; std::vector< clPtr<clGUIPage> > FPages; };

  5. 页面本身充当了clGUIButton对象的容器:

    ```cpp class clGUIButton: public iObject { public: clGUIButton( const LRect& R, const std::string Title,clPtr Page ): FRect(R), FTitle(Title), FPressed(false), FFallbackPage(Page) {}

    virtual void Render(); virtual void OnTouch( const LVector2& Pos, boolTouchState ); ```

  6. 这里最重要的是clGUIButton可以检测按钮内是否包含触摸点:

    cpp virtual bool Contains( const LVector2& Pos ) { return FRect.ContainsPoint( Pos ); } public: LRect FRect; std::string FTitle; bool FPressed; clPtr<clGUIPage> FFallbackPage; };

这两个类足以为我们的游戏构建一个极简的交互式用户界面。

它是如何工作的…

设置用户界面时,我们构建页面并将其添加到全局g_GUI对象:

  g_GUI = new clGUI();
  clPtr<clGUIPage> Page_MainMenu = new clPage_MainMenu;
  clPtr<clGUIPage> Page_Game     = new clPage_Game;
  clPtr<clGUIPage> Page_About    = new clPage_About;

当点击后退按钮时,页面回流如下:

Page_About  Page_MainMenu
Page_Game  Page_MainMenu
Page_MainMenu  exit the application

我们相应地设置了对反向导航目标页面的引用:

  Page_Game->FFallbackPage = Page_MainMenu;
  Page_About->FFallbackPage = Page_MainMenu;
  g_GUI->AddPage( Page_MainMenu );
  g_GUI->AddPage( Page_Game );
  g_GUI->AddPage( Page_About );

主菜单页面还包含一些有用的按钮,这些按钮将帮助玩家在不同页面之间导航:

  Page_MainMenu->AddButton( new clGUIButton( LRect(0.3f, 0.1f, 0.7f, 0.3f ), "New Game", Page_Game  ) );
  Page_MainMenu->AddButton( new clGUIButton( LRect(0.3f, 0.4f, 0.7f, 0.6f ), "About",    Page_About ) );
  Page_MainMenu->AddButton( new clGUIButton( LRect(0.3f, 0.7f, 0.7f, 0.9f ), "Exit",     NULL       ) );

应用从主菜单页面开始:

  g_GUI->SetActivePage( Page_MainMenu );

各个页面的实现非常简单。clPage_About包含一些信息,我们只覆盖 Render()方法:

class clPage_About: public clGUIPage
{
public:
  virtual void Render()
  {
    
  }
};

主菜单页面包含三个按钮,一个用于退出应用,另一个用于开始游戏,还有一个用于进入“关于”页面的按钮:

class clPage_MainMenu: public clGUIPage
{
public:

OnKey()方法也处理后退ESC 按钮。我们使用单一检查,因为我们的抽象层将两个键转换成单一的LK_ESCAPE代码:

  virtual bool OnKey( int Key, bool KeyState )
  {
    if ( Key == LK_ESCAPE ) ExitApp();
    return true;
  }
  
};

游戏页面将渲染、触摸处理和计时事件重定向到全局g_Game对象:

class clPage_Game: public clGUIPage
{
public:
  virtual void OnTouch( const LVector2& Pos, bool TouchState )
  {
    g_Game.OnKey(Pos.x, Pos.y, TouchState);
    clGUIPage::OnTouch(Pos, TouchState);
  }
  virtual void Update(float DT)
  {
    g_Game.Timer( DT );
  }
  virtual void Render()
  {
    RenderGame(&g_Game);
    clGUIPage::Render();
  }
};

还有更多...

作为练习,更多的 UI 控件可以添加到这个极简框架中。添加静态文本标签和图像很容易。更复杂的用户界面控件,如输入框,也可以实现,但需要更多的努力。如果你想为你的游戏构建一个复杂的用户界面,我们建议你在使用一个开源的 C++用户界面库。

另见

  • 实现动画 3D 图像选择器

带 Picasa 下载器的图库

在这个食谱中,我们将把我们的 Picasa 图像下载器与一个基于旋转木马的 3D 图库集成在一起,并将其用作我们游戏中的图片选择页面。

怎么做…

  1. To download the images and track the state of the downloader, we use the sImageDescriptor structure describing the state of any game image:

    cpp class sImageDescriptor: public iObject { public: size_t FID; std::string FURL;

    现在是图像大小代码。我们只支持单一图像类型:小 256 像素宽的预览。多阶段预览可以在游戏首次通过网络加载非常小的图像时实现,比如说不大于 128 像素。然后更大的 256 像素预览替换为在全高清屏幕上给出清晰的预览。当玩家从图库中选择了一张图片后,服务器会显示一个全尺寸的预览。

  2. 前面描述的方法正是我们在林道拼图高清游戏中的做法:

    cpp LPhotoSize FSize;

  3. 我们将该图像的当前状态设置为L_NOTSTARTED初始状态:

    ```cpp LImageState FState;

    clPtr FTexture; clPtr FNewBitmap; sImageDescriptor(): FState(L_NOTSTARTED),FSize(L_PHOTO_SIZE_256) { FTexture = new clGLTexture(); } void StartDownload( bool AsFullSize ); void ImageDownloaded( clPtr Blob ); void UpdateTexture(); }; ```

  4. 图像状态可以是以下状态之一:

    cpp enum LImageState { L_NOTSTARTED, // not started downloading L_LOADING, // download is in progress L_LOADED, // loading is finished L_ERROR // error occured };

  5. 在下载完成后,我们使用FreeImage库从数据块中异步加载图像:

    cpp void sImageDescriptor::ImageDownloaded( clPtr<clBlob> B ) { if ( !B ) { FState = L_ERROR; return; } clPtr<clImageLoadingCompleteCallback> CB =new clImageLoadingComplete( this ); clPtr<clImageLoadTask> LoadTask =new clImageLoadTask( B, 0, CB,g_Events.GetInternalPtr() ); g_Loader->AddTask( LoadTask ); }

  6. 异步加载很重要,因为图像解码可能非常慢,并且会干扰游戏的用户体验。当一个图像被加载并转换成clBitmap后,我们应该更新纹理。纹理更新在 OpenGL 渲染线程上同步完成:

    cpp void sImageDescriptor::UpdateTexture() { this->FState = L_LOADED; FTexture->LoadFromBitmap( FNewBitmap ); }

  7. 让我们更上一层楼,看看如何从服务器获取图像。图像集合从网站检索并存储在clGallery对象中:

    cpp class clGallery: public iObject { public: clGallery(): FNoImagesList(true) {}

  8. 返回全尺寸图片网址:

    cpp std::string GetFullSizeURL(int Idx) const { return ( Idx < (int)FURLs.size() ) ?Picasa_GetDirectImageURL(FURLs[Idx], L_PHOTO_SIZE_ORIGINAL ): std::string(); } size_t GetTotalImages() const{ return FImages.size(); } clPtr<sImageDescriptor> GetImage( size_t Idx ) const{ return ( Idx < FImages.size() ) ? FImages[Idx] : NULL; } …

  9. 重新开始下载所有未加载的图片:

    cpp void ResetAllDownloads(); bool StartListDownload(); …

  10. 我们存储所有图片的基本网址,以及图片本身:

    cpp std::vector<std::string> FURLs; std::vector< clPtr<sImageDescriptor> > FImages; };

  11. 为了解码图像列表,我们使用来自第 3 章联网 :

    ```cpp class clListDownloadedCallback: public clDownloadCompleteCallback { public: clListDownloadedCallback( const clPtr& G ): FGallery(G) {} virtual void Invoke() { FGallery->ListDownloaded( FResult ); }

    clPtr FGallery; };

    void clGallery::ListDownloaded( clPtr B ) { if ( !B ) { FNoImagesList = true; return; } ```

    的 Picasa 下载器代码 12. 解析对应于从 Picasa 加载的 XML 图像列表的数据块:

    cpp FURLs.clear(); void* Data = B->GetData(); size_t DataSize = B->GetSize(); Picasa_ParseXMLResponse(std::string( ( char* )Data, DataSize ), FURLs );

  12. 更新描述符,开始下载图片:

    cpp FImages.clear(); for ( size_t j = 0 ; j != FURLs.size() ; j++ ) { LPhotoSize Size = L_PHOTO_SIZE_256; std::string ImgUrl = Picasa_GetDirectImageURL(FURLs[j], Size); clPtr<sImageDescriptor> Desc = new sImageDescriptor(); Desc->FSize = Size; Desc->FURL = ImgUrl; Desc->FID = j; FImages.push_back(Desc); Desc->StartDownload( true ); } FNoImagesList = false; }

  13. 一旦图像加载完成,任务会向主线程分派一个clBitmap::Load2DImage()调用,这样就可以更新 OpenGL 纹理:

    cpp class clImageLoadTask: public iTask { public: … virtual void Run() { clPtr<ImageLoadTask> Guard( this ); clPtr<iIStream> In = (FSourceStream == NULL) ?g_FS->ReaderFromBlob( FSource ) : FSourceStream; FResult = new clBitmap(); FResult->Load2DImage(In, true); if ( FCallback ) { FCallback->FTaskID = GetTaskID(); FCallback->FResult = FResult; FCallback->FTask = this; FCallbackQueue->EnqueueCapsule( FCallback ); FCallback = NULL; } } … };

完整的源代码可以在5_Puzzle项目中找到。

它是如何工作的…

下载在全局g_Downloader对象中执行,下载数据的实际解码使用FreeImage库完成。

另见

实现完整的拼图游戏

最后,我们手头上有所有的零件,可以把它们组合在一起成为一个益智游戏应用。

做好准备

从补充资料中构建并运行示例5_Puzzle。这个例子和本书中的其他例子一样,在安卓和 Windows 上运行。

怎么做…

  1. 我们从用新的页面clPage_Gallery增加我们的3_UIPrototype项目开始。该页面将渲染和更新委托给全局g_Flow对象:

    cpp class clPage_Gallery: public clGUIPage { public: … virtual void Render() { RenderDirect( g_Flow ); } virtual void Update(float DT) { g_Flow->FFlinger->Update(DT); } private: void RenderDirect( clPtr<clFlowUI> Control ); };

  2. RenderDirect()方法本质上是在章节中实现动画 3D 图像选择器配方的RenderDirect()的稍微修改版本。只有两个不同之处——我们用clCanvas::Rect3D()调用替换线框四重渲染(渲染一个有纹理的三维矩形),并使用来自g_Gallery对象的纹理,这在本章最近的图片库中用 Picasa 下载器描述:

    cpp void RenderDirect( clPtr<clFlowUI> Control ); { …

  3. 渲染顺序从左到右,防止图像不正确重叠:

    cpp int ImgOrder[] = { CurImg - 3, CurImg - 2, CurImg - 1,CurImg + 3, CurImg + 2, CurImg + 1, CurImg };

  4. 根据预定义的顺序渲染七个纹理化的三维矩形。如果没有图像可用,我们使用占位符纹理【T0:

    cpp for ( int in_i = 0 ; in_i < 7 ; in_i++ ) { … if ( i < Num && i > -1 ) { … clPtr<sImageDescriptor> Img =g_Gallery->GetImage( i ); clPtr<clGLTexture> Txt =Img ? Img->FTexture : g_Texture; g_Canvas->Rect3D( Control->FProjection,Control->FView, Pt[1], Pt[0], Pt[3], Pt[2], Txt,NULL ); } } }

  5. 一旦我们有了一个分成页面的用户界面,我们就可以将所有的渲染、更新和输入委托给我们的g_GUI对象。引擎回调的实现很简单:

    cpp void OnDrawFrame() { g_GUI->Render(); } void OnKey( int code, bool pressed ) { g_GUI->OnKey( g_Pos, code, pressed ); }

  6. 在定时器更新中,我们应该处理其他线程发布的事件:

    cpp void OnTimer( float Delta ) { g_Events->DemultiplexEvents(); g_GUI->Update( Delta ); }

  7. Tap 处理有点复杂,因为我们必须额外存储图库中的标志。为了简单起见,我们将其实现为全局变量g_InGallery :

    cpp void OnMouseDown( int btn, const LVector2& Pos ) { g_Pos = Pos; g_GUI->OnTouch( Pos, true ); if ( g_InGallery ) { g_MousePos = Pos; g_MouseTime = Env_GetSeconds(); g_Flow->FFlinger->OnTouch( true ); } }

回调OnMouseMove()OnMouseUp()类似,可以在5_Puzzle/main.cpp文件中找到。

它是如何工作的…

让我们简单地看一下这个游戏。主菜单如下图所示:

How it works…

点击新游戏显示 3D 旋转木马,图片来自皮卡萨,如下图所示:

How it works…

向左或向右滚动以选择所需的图像。轻点它。游戏字段打开时会显示照片的混洗拼贴,如下图所示:

How it works…

移动图块以恢复原始图像。

还有更多...

以下是留下的一些不错的特性,它们大大增加了拼图的可用性,以及你可以作为一个练习来实现:

  • 实现不同的平铺网格。4 x 4 很容易玩。8 x 14 相当有挑战性。甚至更大的网格在 10 英寸平板电脑上看起来也不错。
  • 将正确组装的瓷砖缝合在一起,并将其作为一个整体移动。
  • 您可以使用填充算法来查找相邻的图块。
  • 保存游戏状态,这样玩家就可以从他们停止的地方继续游戏。当有来电时,保存游戏也是一个好主意。可以在OnStop()回调中做。
  • 多阶段预览-在 3D 轮播中加载小的低分辨率预览。加载粗略预览后,获取更高分辨率的预览图像。一旦玩家点击他想玩的图像,下载高分辨率的图像。这将使游戏在全高清平板设备上看起来清晰。
  • 实现不同的图库。你可以从 Flickr 开始,就像第三章联网中的食谱从 Flickr 和 Picasa 获取照片列表中描述的那样。

另见