AD商业广告自由选择
AD商业广告自由选择

【HGE】绘图底层

正文概述 开源人   2024-11-03 15:51:10  

HGE是基于DX8.0的二维游戏引擎,多年没有更新了。i2v1234FCOM专注游戏工具及源码例子分享

而我们知道Dx8.0跟DX9.0C是不同层次的。其实基本绘图逻辑差别不是太大,只是性能方面肯定不在一个水平上面。i2v1234FCOM专注游戏工具及源码例子分享

让我感觉很大困惑的是,HGE的绘图结构效率到底适不适合即时大型网络游戏渲染?因为它的绘图逻辑是基于以前的DX7.0的绘图思想。i2v1234FCOM专注游戏工具及源码例子分享

先分析它的架构:i2v1234FCOM专注游戏工具及源码例子分享

 
 1 (*
 2 ** HGE Primitive type constants
 3 *)
 4 const
 5   HGEPRIM_LINES    = 2;
 6   HGEPRIM_TRIPLES  = 3;
 7   HGEPRIM_QUADS    = 4;
 8 
 9 (*
10 ** HGE Vertex structure
11 *)
12 type
13   THGEVertex = record
14     X, Y: Single;   // screen position
15     Z: Single;      // Z-buffer depth 0..1
16     Col: Longword;  // color
17     TX, TY: Single; // texture coordinates
18   end;
19   PHGEVertex = ^THGEVertex;
20   THGEVertexArray = array [0..MaxInt div 32 - 1] of THGEVertex;
21   PHGEVertexArray = ^THGEVertexArray;
22   TCustomVertex = packed record
23     x, y, z: single; // Position
24     rhw: single; // Reciprocal of homogeneous w
25     Col:  Longword; // Vertex Color
26     tu, tv: single; // Texture coordinates
27   end;
28   PCustomVertex = ^TCustomVertex;
29 
30 (*
31 ** HGE Triple structure三角形结构
32 *)
33 type
34   THGETriple = record
35     V: array [0..2] of THGEVertex;
36     Tex: ITexture;
37     Blend: Integer;
38   end;
39   PHGETriple = ^THGETriple;
40 
41 (*
42 ** HGE Quad structure四边形结构
43 *)
44 type
45   THGEQuad = record
46     V: array [0..3] of THGEVertex;
47     Tex: ITexture;
48     Blend: Integer;
49   end;
50   PHGEQuad = ^THGEQuad;
 

i2v1234FCOM专注游戏工具及源码例子分享
FVF常量定义:i2v1234FCOM专注游戏工具及源码例子分享

1 const
2   D3DFVF_HGEVERTEX   = D3DFVF_XYZ or D3DFVF_DIFFUSE or D3DFVF_TEX1;
3   VertexDef = D3DFVF_XYZRHW or D3DFVF_DIFFUSE or D3DFVF_TEX1;
4   VERTEX_BUFFER_SIZE = 4000; //静态缓冲区的大小基本参数

i2v1234FCOM专注游戏工具及源码例子分享
上面这个过程在D3D编程里面,大家应该很熟悉了,定义顶点结构、定义FVF常量结构。i2v1234FCOM专注游戏工具及源码例子分享

接着应该是创建顶点缓冲和索引缓冲,HGE定义的是静态缓冲,也就是说缓冲区是在显示卡内存里面的。i2v1234FCOM专注游戏工具及源码例子分享

继续看它创建缓冲区的代码:i2v1234FCOM专注游戏工具及源码例子分享

其实这部分是非常关键和重要的,直接影响到引擎的性能。i2v1234FCOM专注游戏工具及源码例子分享

 


 
  1 function THGEImpl.InitLost: Boolean;  //接口的子类实现部分
  2 var
  3   Target: IInternalTarget;
  4   PIndices: PWord;
  5   N: Word;
  6   I: Integer;
  7 begin
  8   Result := False;
  9 
 10 // Store render target
 11 
 12   FScreenSurf := nil;
 13   FScreenDepth := nil;
 14 
 15   {$IFDEF HGE_DX8}
 16   FD3DDevice.GetRenderTarget(FScreenSurf);
 17   {$ELSE}
 18   FD3DDevice.GetRenderTarget(0,FScreenSurf);
 19   {$ENDIF}
 20   FD3DDevice.GetDepthStencilSurface(FScreenDepth);
 21 
 22   for I := 0 to FTargets.Count - 1 do begin
 23     Target := IInternalTarget(FTargets[I]);
 24     Target.Lost;
 25   end;
 26 
 27 // Create Vertex buffer     
 28   {$IFDEF HGE_DX8}
 29   if (Failed(FD3DDevice.CreateVertexBuffer(VERTEX_BUFFER_SIZE * SizeOf(THGEVertex),
 30     D3DUSAGE_WRITEONLY,D3DFVF_HGEVERTEX,D3DPOOL_DEFAULT,FVB)))
 31   {$ELSE}//这些是DX9部分
 32   if (Failed(FD3DDevice.CreateVertexBuffer(VERTEX_BUFFER_SIZE * SizeOf(THGEVertex),
 33     D3DUSAGE_WRITEONLY,D3DFVF_HGEVERTEX,D3DPOOL_DEFAULT,FVB,nil)))
 34 
 35 //D3DUSAGE_WRITEONLY指定应用程序只能写缓存。它允许驱动程序分配最适合的内存地址作为写缓存。注意如果从创建好的这种缓存中读数据,将会返回错误信息。
 36 
 37 
 38   {$ENDIF}
 39   then begin
 40     PostError('Can''t create D3D vertex buffer');
 41     Exit;
 42   end;
 43 
 44   {$IFDEF HGE_DX8}
 45   FD3DDevice.SetVertexShader(D3DFVF_HGEVERTEX);
 46   FD3DDevice.SetStreamSource(0,FVB,SizeOf(THGEVertex));
 47   {$ELSE}//这些是DX9部分
 48   FD3DDevice.SetVertexShader(nil);
 49   FD3DDevice.SetFVF(D3DFVF_HGEVERTEX);
 50   FD3DDevice.SetStreamSource(0,FVB,0,SizeOf(THGEVertex));
 51   {$ENDIF}
 52 
 53 // Create and setup Index buffer
 54 
 55   {$IFDEF HGE_DX8}
 56   if (Failed(FD3DDevice.CreateIndexBuffer(VERTEX_BUFFER_SIZE * 6 div 4 * SizeOf(Word),
 57     D3DUSAGE_WRITEONLY,D3DFMT_INDEX16,D3DPOOL_DEFAULT,FIB)))
 58   {$ELSE}//这些是DX9部分
 59   if (Failed(FD3DDevice.CreateIndexBuffer(VERTEX_BUFFER_SIZE * 6 div 4 * SizeOf(Word),
 60     D3DUSAGE_WRITEONLY,D3DFMT_INDEX16,D3DPOOL_DEFAULT,FIB,nil)))
 61   {$ENDIF}
 62   then begin
 63     PostError('Can''t create D3D index buffer');
 64     Exit;
 65   end;
 66 
 67   N := 0;
 68   {$IFDEF HGE_DX8}
 69   if (Failed(FIB.Lock(0,0,PByte(PIndices),0))) then
 70   {$ELSE}//这些是DX9部分
 71   if (Failed(FIB.Lock(0,0,Pointer(PIndices),0))) then
 72   {$ENDIF}
 73   begin
 74     PostError('Can''t lock D3D index buffer');
 75     Exit;
 76   end;
 77 
 78   for I := 0 to (VERTEX_BUFFER_SIZE div 4) - 1 do begin
 79     PIndices^ := N  ; Inc(PIndices);
 80     PIndices^ := N+1; Inc(PIndices);
 81     PIndices^ := N+2; Inc(PIndices);
 82     PIndices^ := N+2; Inc(PIndices);
 83     PIndices^ := N+3; Inc(PIndices);
 84     PIndices^ := N;   Inc(PIndices);
 85     Inc(N,4);
 86   end;
 87 
 88   FIB.Unlock;
 89   {$IFDEF HGE_DX8}
 90   FD3DDevice.SetIndices(FIB,0);
 91   {$ELSE}//这些是DX9部分
 92   FD3DDevice.SetIndices(FIB);
 93   {$ENDIF}
 94 
 95 // Set common render states
 96 
 97   //pD3DDevice->SetRenderState( D3DRS_LASTPIXEL, FALSE );  ignore this
 98   FD3DDevice.SetRenderState(D3DRS_CULLMODE,D3DCULL_NONE);
 99   FD3DDevice.SetRenderState(D3DRS_LIGHTING,0);
100 
101   FD3DDevice.SetRenderState(D3DRS_ALPHABLENDENABLE,1);
102   FD3DDevice.SetRenderState(D3DRS_SRCBLEND,D3DBLEND_SRCALPHA);
103   FD3DDevice.SetRenderState(D3DRS_DESTBLEND,D3DBLEND_INVSRCALPHA);
104 
105   FD3DDevice.SetRenderState(D3DRS_ALPHATESTENABLE,1);
106   FD3DDevice.SetRenderState(D3DRS_ALPHAREF,1);
107   FD3DDevice.SetRenderState(D3DRS_ALPHAFUNC,D3DCMP_GREATEREQUAL);
108 
109   FD3DDevice.SetTextureStageState(0,D3DTSS_COLOROP,  D3DTOP_MODULATE);
110   FD3DDevice.SetTextureStageState(0,D3DTSS_COLORARG1,D3DTA_TEXTURE);
111   FD3DDevice.SetTextureStageState(0,D3DTSS_COLORARG2,D3DTA_DIFFUSE);
112 
113   FD3DDevice.SetTextureStageState(0,D3DTSS_ALPHAOP,  D3DTOP_MODULATE);
114   FD3DDevice.SetTextureStageState(0,D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
115   FD3DDevice.SetTextureStageState(0,D3DTSS_ALPHAARG2,D3DTA_DIFFUSE);
116 
117   {$IFDEF HGE_DX8}
118   FD3DDevice.SetTextureStageState(0,D3DTSS_MIPFILTER, D3DTEXF_POINT);
119   {$ELSE}//这些是DX9部分
120   FD3DDevice.SetSamplerState(0,D3DSAMP_MIPFILTER, D3DTEXF_POINT);
121   {$ENDIF}
122 
123   if (FTextureFilter) then begin
124     {$IFDEF HGE_DX8}
125     FD3DDevice.SetTextureStageState(0,D3DTSS_MAGFILTER,D3DTEXF_LINEAR);
126     FD3DDevice.SetTextureStageState(0,D3DTSS_MINFILTER,D3DTEXF_LINEAR);
127     {$ELSE}//这些是DX9部分
128     FD3DDevice.SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_LINEAR);
129     FD3DDevice.SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_LINEAR);
130     {$ENDIF}
131   end else begin
132     {$IFDEF HGE_DX8}
133     FD3DDevice.SetTextureStageState(0,D3DTSS_MAGFILTER,D3DTEXF_POINT);
134     FD3DDevice.SetTextureStageState(0,D3DTSS_MINFILTER,D3DTEXF_POINT);
135     {$ELSE}//这些是DX9部分
136     FD3DDevice.SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_POINT);
137     FD3DDevice.SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_POINT);
138     {$ENDIF}
139   end;
140 
141   FPrim := 0;
142   FCurPrimType := HGEPRIM_QUADS;
143   FCurBlendMode := BLEND_DEFAULT;
144   FCurTexture := nil;
145 
146   FD3DDevice.SetTransform(D3DTS_VIEW,FMatView);
147   FD3DDevice.SetTransform(D3DTS_PROJECTION,FMatProj);
148 
149   Result := True;
150 end;
 

i2v1234FCOM专注游戏工具及源码例子分享
这份代码是HGE包含DX9的,其实很不好,DX9跟DX8在一些方面是不兼容。i2v1234FCOM专注游戏工具及源码例子分享

 显然顶点缓冲区是只写属性,因为最后是从索引缓冲里面读取数据。i2v1234FCOM专注游戏工具及源码例子分享

这个顶点缓冲的大小是固定的,很难说够不够用。如果不够用怎么办?还没有看到这部分的代码在那里。i2v1234FCOM专注游戏工具及源码例子分享

或者说,根本没有完全使用完整个顶点缓冲区的容量。i2v1234FCOM专注游戏工具及源码例子分享

下面再看看他的代码:i2v1234FCOM专注游戏工具及源码例子分享

 
 1 function THGEImpl.Gfx_BeginScene(const Target: ITarget): Boolean;
 2 var
 3   {$IFDEF HGE_DX8}
 4   Surf, Depth: IDirect3DSurface8;
 5   {$ELSE}
 6   Surf, Depth: IDirect3DSurface9;
 7   {$ENDIF}
 8   HR: HResult;
 9 begin
10   Result := False;
11 
12   HR := FD3DDevice.TestCooperativeLevel;
13   if (HR = D3DERR_DEVICELOST) then
14     Exit;
15   if (HR = D3DERR_DEVICENOTRESET) then
16     if (not GfxRestore) then
17       Exit;
18 
19   if Assigned(FVertArray) then begin
20     PostError('Gfx_BeginScene: Scene is already being rendered');
21     Exit;
22   end;
23 
24   if (Target <> FCurTarget) then begin
25     if Assigned(Target) then begin
26       Target.Tex.Handle.GetSurfaceLevel(0,Surf);
27       Depth := (Target as IInternalTarget).Depth;
28     end else begin
29       Surf := FScreenSurf;
30       Depth := FScreenDepth;
31     end;
32 
33     {$IFDEF HGE_DX8}
34     if (Failed(FD3DDevice.SetRenderTarget(Surf,Depth)))
35     {$ELSE}
36     if (Failed(FD3DDevice.SetRenderTarget(0,Surf)))
37     {$ENDIF}
38     then begin
39       PostError('Gfx_BeginScene: Can''t set render target');
40       Exit;
41     end;
42     if Assigned(Target) then begin
43       Surf := nil;
44       if Assigned((Target as IInternalTarget).Depth) then
45         FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_TRUE)
46       else
47         FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_FALSE);
48       SetProjectionMatrix(Target.Width,Target.Height);
49     end else begin
50       if (FZBuffer) then
51         FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_TRUE)
52       else
53         FD3DDevice.SetRenderState(D3DRS_ZENABLE,D3DZB_FALSE);
54       SetProjectionMatrix(FScreenWidth,FScreenHeight);
55     end;
56 
57     FD3DDevice.SetTransform(D3DTS_PROJECTION,FMatProj);
58     D3DXMatrixIdentity(FMatView);
59     FD3DDevice.SetTransform(D3DTS_VIEW,FMatView);
60 
61     FCurTarget := Target;
62   end;
63   FD3DDevice.BeginScene;
64   {$IFDEF HGE_DX8}
65   FVB.Lock(0,0,PByte(FVertArray),0);
66   {$ELSE}
67   FVB.Lock(0,0,Pointer(FVertArray),0);
68   {$ENDIF}
69   Result := True;
70 end;
 

i2v1234FCOM专注游戏工具及源码例子分享
应该看到了,这个是HGE每一帧里面需要调用的开始渲染函数。i2v1234FCOM专注游戏工具及源码例子分享

在每一帧开始的时候就锁定顶点缓冲,然后把数据拷贝进缓冲区里面。i2v1234FCOM专注游戏工具及源码例子分享

那么我们了解到的情况是:经常对静态缓冲加解锁是不明智的,因为只有等驱动完成了所有挂起的命令之后才能返回该缓冲的指针。如果经常这样做,这会导致CPU和GPU很多不必要的同步,这样性能将会变得很差。i2v1234FCOM专注游戏工具及源码例子分享

何况是每一帧开始之后才填充数据,这种方式跟以前的DDRAW7的绘图模式完全是一样的。i2v1234FCOM专注游戏工具及源码例子分享

 
procedure THGEImpl.Gfx_EndScene;
begin
  RenderBatch(True);
  FD3DDevice.EndScene;
  if (FCurTarget = nil) then
    FD3DDevice.Present(nil,nil,0,nil);
end;
 

i2v1234FCOM专注游戏工具及源码例子分享
结束渲染之前调用了一次RenderBatch函数,并且传入参数为True。i2v1234FCOM专注游戏工具及源码例子分享

看看这个函数的功能:i2v1234FCOM专注游戏工具及源码例子分享

 
procedure THGEImpl.RenderBatch(const EndScene: Boolean);
begin
  if Assigned(FVertArray) then begin
    FVB.Unlock;
    if (FPrim <> 0) then begin
      case FCurPrimType of
        HGEPRIM_QUADS:
          {$IFDEF HGE_DX8}                     
          FD3DDevice.DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,FPrim shl 2,0,FPrim shl 1);
          {$ELSE}
          FD3DDevice.DrawIndexedPrimitive(D3DPT_TRIANGLELIST,0,0,FPrim shl 2,0,FPrim shl 1);
          {$ENDIF}
        HGEPRIM_TRIPLES:
          FD3DDevice.DrawPrimitive(D3DPT_TRIANGLELIST,0,FPrim);
        HGEPRIM_LINES:
          FD3DDevice.DrawPrimitive(D3DPT_LINELIST,0,FPrim);
      end;

      FPrim := 0;       //绘制完毕,清零了,好像不用累计
    end;

    if (EndScene) then        //结束渲染之前执行
      FVertArray := nil       
    else
      {$IFDEF HGE_DX8}
      FVB.Lock(0,0,PByte(FVertArray),0);
      {$ELSE}
      FVB.Lock(0,0,Pointer(FVertArray),0);  //常规的做法,我们是使用一个无类型指针,这里他使用的是FVertArray这样的指针
      {$ENDIF}
  end;
end;
 

 i2v1234FCOM专注游戏工具及源码例子分享

1 FVertArray: PHGEVertexArray;

这个函数是创建精灵的时候,需要调用的函数,也是其它单元经常调用的。i2v1234FCOM专注游戏工具及源码例子分享

 
 1 procedure THGEImpl.Gfx_RenderQuad(const Quad: THGEQuad);
 2 begin
 3   if Assigned(FVertArray) then begin
 4     if (FCurPrimType <> HGEPRIM_QUADS)
 5       or (FPrim >= VERTEX_BUFFER_SIZE div HGEPRIM_QUADS)
 6       or (FCurTexture <> Quad.Tex)
 7       or (FCurBlendMode <> Quad.Blend)
 8     then begin
 9       RenderBatch;
10       FCurPrimType := HGEPRIM_QUADS;
11       if (FCurBlendMode <> Quad.Blend) then
12         SetBlendMode(Quad.Blend);
13       if (Quad.Tex <> FCurTexture) then begin
14         if Assigned(Quad.Tex) then
15           FD3DDevice.SetTexture(0,Quad.Tex.Handle)
16         else
17           FD3DDevice.SetTexture(0,nil);
18         FCurTexture := Quad.Tex;
19       end;
20     end;
21 
22     Move(Quad.V,FVertArray[FPrim * HGEPRIM_QUADS],
23       SizeOf(THGEVertex) * HGEPRIM_QUADS);
24     Inc(FPrim);
25   end;
26 end;
 

i2v1234FCOM专注游戏工具及源码例子分享
这个函数的调用刚好处于开始渲染和结束渲染之间。Move把Quad的数据复制到顶点缓冲里面。要知道Quad结构的数据在被调用之前就已经填充好了。然后再复制入缓冲区里面。i2v1234FCOM专注游戏工具及源码例子分享

就是说,从渲染开始到渲染结束,都是处于Lock锁定的状态下。所以很难看出HGE的绘图高效在那里。显然这样的渲染方式不适合大量绘制图元,更不用说应用于大型或者超大型网络游戏里面了。i2v1234FCOM专注游戏工具及源码例子分享

因为锁定状态是按照默认锁定的,就是在GPU绘图这段时间里面,CPU就一直在等待之中,而且系统就处于挂起状态,如果是绘制大量图元呢,结果是可想而知的。i2v1234FCOM专注游戏工具及源码例子分享

按照我们常规的逻辑,当引擎渲染开始之后,最理想的状态是,这个时候不必再去计算和处理各种数据或者是再锁定缓冲区去修改里面的数据,而是按照渲染序列,一次性批量地进行渲染。i2v1234FCOM专注游戏工具及源码例子分享

说真的,我看了很久,看不出HGE能够胜任大型网络游戏的优点在那里。怎么看,都好像适合以前那些小型游戏开发。i2v1234FCOM专注游戏工具及源码例子分享

网上的资料都是研究怎么去学习,还没有看到有人去研究它的实现代码结构方面。希望了解这个引擎的人说下。i2v1234FCOM专注游戏工具及源码例子分享

 i2v1234FCOM专注游戏工具及源码例子分享
好像也不对,看一段代码:i2v1234FCOM专注游戏工具及源码例子分享

 
 1 hge->Gfx_BeginScene();     //开始渲染,LOCK锁定缓冲区
 2     bgspr->Render(0,0);    //第一次调用 FHGE.Gfx_RenderQuad(FQuad);
 3     
 4     for(i=0;i<nObjects;i++)
 5     {
 6         pObjects[i].x+=pObjects[i].dx*dt;
 7         if(pObjects[i].x>SCREEN_WIDTH || pObjects[i].x<0) pObjects[i].dx=-pObjects[i].dx;
 8         pObjects[i].y+=pObjects[i].dy*dt;
 9         if(pObjects[i].y>SCREEN_HEIGHT || pObjects[i].y<0) pObjects[i].dy=-pObjects[i].dy;
10         pObjects[i].scale+=pObjects[i].dscale*dt;
11         if(pObjects[i].scale>2 || pObjects[i].scale<0.5) pObjects[i].dscale=-pObjects[i].dscale;
12         pObjects[i].rot+=pObjects[i].drot*dt;
13         
14         spr->SetColor(pObjects[i].color); 
           //循环调用 FHGE.Gfx_RenderQuad(FQuad);
15         spr->RenderEx(pObjects[i].x, pObjects[i].y, pObjects[i].rot, pObjects[i].scale);
16     }
17 
18     fnt->printf(7,7,"UP and DOWN to adjust number of hares: %d\nSPACE to change blending mode: %d\nFPS: %d", nObjects, nBlend, hge->Timer_GetFPS());
19     hge->Gfx_EndScene();   //解锁缓冲区,结束渲染
 

 i2v1234FCOM专注游戏工具及源码例子分享

 
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)  //程序入口

``````````````
        fnt=new hgeFont("font2.fnt");
        spr=new hgeSprite(tex,0,0,64,64);
        spr->SetHotSpot(32,32);              //在这里生成一个矩形





------------------------------------------

procedure THGESprite.Render(const X, Y: Single);
var
  TempX1, TempY1, TempX2, TempY2: Single;
begin
  TempX1 := X - FHotX;
  TempY1 := Y - FHotY;
  TempX2 := X + FWidth - FHotX;
  TempY2 := Y + FHeight - FHotY;

  FQuad.V[0].X := TempX1; FQuad.V[0].Y := TempY1;
  FQuad.V[1].X := TempX2; FQuad.V[1].Y := TempY1;
  FQuad.V[2].X := TempX2; FQuad.V[2].Y := TempY2;
  FQuad.V[3].X := TempX1; FQuad.V[3].Y := TempY2;

  FHGE.Gfx_RenderQuad(FQuad);
end;


procedure THGESprite.SetHotSpot(const X, Y: Single);
begin
  FHotX := X;
  FHotY := Y;
end;
 

 i2v1234FCOM专注游戏工具及源码例子分享

i2v1234FCOM专注游戏工具及源码例子分享
显然在锁定缓冲区的同时,进行各种运算生成数据,然后批量地进行绘制图元。从流程可以看出来,第一次生成——就是说初次复制到缓冲区的数据是不被立即渲染,而是在第二次生成数据并且调用Gfx_RenderQuad这个函数的时候,才被渲染。也就是上一次的数据放到下一次进行渲染,这样形成了一个延迟渲染的流程。显然这些针对的是批量渲染算法。i2v1234FCOM专注游戏工具及源码例子分享

 i2v1234FCOM专注游戏工具及源码例子分享

显然它只需要锁定一次缓冲区,就可以批量地绘制大量的图元,同时它不需要等到把所有的数据填充入缓冲区之后,再批量地绘制。好像比较适合数据量大的情况,当然这些对于绘制二维图元来说,应该说是足够了。i2v1234FCOM专注游戏工具及源码例子分享

解决方案在另一个帖子里面。i2v1234FCOM专注游戏工具及源码例子分享



声明:本文系互联网搜索而收集整理,不以盈利性为目的,文字、图文资料源于互联网且共享于互联网。
如有侵权,请联系 yao4fvip#qq.com (#改@) 删除。