这是“
使用 C# 开发智能手机软件:推箱子 ”系列文章的第十一篇。在这篇文章中,介绍 Common/Env.cs 源程序文件。这个源程序文件中包含表示“工作环境”的密封类 Env 。也就是说,主程序中重要的变量都封装在这个类中,作为整个程序的“工作环境”。她还起着桥梁作用,其中两个字段:
DataFile db; // 数据文件
ConfigFile cfg; // 配置文件
正是我们以前介绍过的管理数据文件的密封类 DataFile 和管理配置文件的密封类 ConfigFile 的实例。密封类 Env 中的不少属性和方法是通过这两个字段调用其各自的属性和方法。
下面对密封类 Env 中的一些方法作点说明:
GetClientSize 方法用来计算当使用标准箱子尺寸时主窗体客户区的尺寸。该方法仅当程序运行在计算机上时才会被调用,使主窗体的尺寸根据当前关的尺寸自动调整。程序运行在智能手机时是不会被调用的,因为在智能手机上本程序并不改变主窗体的尺寸。
SetBoxInfo 方法的作用是根据客户区尺寸计算箱子的尺寸,并相应设定要显示的图形单元。图形单元共有 24x24、20x20、16x16 和 12x12 四种尺寸,如下所示:
如果使用 12x12 的图形单元还不能在客户区完整显示地图的话,可能这一关的游戏就无法玩了。
Draw 方法用来更新主窗体客户区,也就是在主窗体客户区画出本关的地图,并根据玩家的动作随时更新地图。
Design 方法实现在设计模式下,当鼠标点击时要采取的动作。
StepIt 方法实现工人往指定方向前进一步(可能推着箱子)。
Back 方法实现工人后退一步(可能连带箱子一起后退)。
GetMoveInfo 方法寻找一条将工人移动到鼠标点击的位置的路线。她调用我们以前介绍过的静态类 FindPath 的 Seek 方法来寻找最短路线。
GetPushInfo 方法给出将箱子推动到鼠标点击的位置所需的信息。
到此为止,Common 目录下所有源程序文件都介绍完了,这些源程序文件中包含的类是实现整个程序功能的基础。在随后的文章中将介绍 Windows 目录下的源程序文件,她们包含的类实现整个程序的用户界面。
下面就是密封类 Env 的源程序代码,虽然稍微长了一点,但是里面的注释比较详细,应该不难理解。
1
using
System;
2
using
System.Drawing;
3
using
System.Collections.Generic;
4
5
namespace
Skyiv.Ben.PushBox.Common
6
{ 7 /**/ /// <summary> 8 /// 工作环境 9 /// </summary> 10 sealed class Env : IDisposable 11 { 12 DataFile db; // 数据文件 13 ConfigFile cfg; // 配置文件 14 string errorMsg; // 错误信息 15 string debugMsg; // 调试信息 16 bool isReplay; // 是否正在回放 17 Action active; // 模式: 正常 新建 编辑 删除 18 byte pen; // 设计时的笔 19 Bitmap img; // 图形单元, 横向被均匀分为八份 20 Stack < Step > stack; // 历史路线, 用于后退功能 21 Size clientSize; // 工作区域尺寸(以像素为单位) 22 Size boxSize; // 图形元素尺寸(以像素为单位) 23 Point toPixel; // 将要到达的位置(以像素为单位) 24 Point worker; // 当前工人位置(以单元格为单位) 25 int pushSteps; // 推动着箱子走的步数 26 int levelOem; // 原来的关数,仅用于“菜单 -> 数据 -> 设计 -> 新建”放弃后恢复现场 27 28 public string ErrorMsg { get { return errorMsg; } } 29 public string DebugMsg { get { return debugMsg; } } 30 public string [] Groups { get { return cfg.Groups; } } 31 public int Group { get { return cfg.Group; } set { cfg.Group = value; } } 32 public int Level { get { return cfg.Levels[Group]; } set { cfg.Levels[Group] = value; } } 33 public int LeveLOem { get { return levelOem; } } 34 public int MaxLevel { get { return db.MaxLevel; } } 35 public string Steps { get { return cfg.Steps; } } 36 public Size LevelSize { get { return db.LevelSize; } } 37 public Size ClientSize { set { clientSize = value; } } 38 public Point ToPixel { set { toPixel = value; } } 39 public int MaxLevelSize { get { return cfg.MaxLevelSize; } set { cfg.MaxLevelSize = value; } } 40 public int StepDelay { get { return cfg.StepDelay; } set { cfg.StepDelay = value; } } 41 public int ReplayDelay { get { return cfg.ReplayDelay; } set { cfg.ReplayDelay = value; } } 42 public Action Active { get { return active; } set { active = value; } } 43 public byte Pen { get { return pen; } set { pen = value; } } 44 public bool HasError { get { return ! string .IsNullOrEmpty(errorMsg); } } 45 public bool HasWorker { get { return db.HasWorker; } } 46 public bool CanUndo { get { return stack.Count != 0 ; } } 47 public bool CanReplay { get { return db.IsFinished && ! CanUndo; } } 48 public bool IsSave { get { return cfg.IsSave; } set { cfg.IsSave = value; } } 49 public bool IsFinish { get { return db.Tasks == db.Boths; } } 50 public bool IsReplay { get { return isReplay; } set { isReplay = value; } } 51 public bool IsDesign { get { return active != Action.None; } } 52 53 public Env() 54 { 55 stack = new Stack < Step > (); 56 cfg = new ConfigFile(); 57 db = new DataFile(); 58 Init(); 59 } 60 61 /**/ /// <summary> 62 /// 状态栏信息 63 /// </summary> 64 public string StatusMessage 65 { 66 get 67 { 68 return HasError ? " 请点击“菜单 -> 帮助 -> 错误信息” " : string .Format( 69 " {0} {1}/{2} {3} {4} {5} [{6}] {7} " , 70 (active == Action.Create) ? ' + ' : (active == Action.Edit) ? ' = ' : isReplay ? " |/-\\ " [stack.Count % 4 ] : ' > ' , 71 Level + 1 , MaxLevel, Pub.ToString(LevelSize), 72 IsDesign ? string .Format( " {0}={1} " , db.Boxs, db.Slots) : string .Format( " {0}/{1} " , db.Boths, db.Tasks), 73 IsDesign ? Block.GetPenName(pen) : string .Format( " {0}({1}) " , stack.Count, pushSteps), 74 IsDesign ? (active == Action.Create ? " 新建 " : " 编辑 " ) : db.IsFinished ? 75 string .Format( " {0}({1}) " , db.MovedSteps, db.PushedSteps) : string .Empty, 76 db.GroupName); 77 } 78 } 79 80 public void Dispose() 81 { 82 db.Dispose(); 83 } 84 85 public void Init() 86 { 87 active = Action.None; 88 pen = Block.Land; 89 stack.Clear(); 90 SetExceptionMessage(null ); 91 } 92 93 void SetExceptionMessage(Exception ex) 94 { 95 errorMsg = Pub.GetMessage(ex, false ); 96 debugMsg = Pub.GetMessage(ex, true ); 97 } 98 99 /**/ /// <summary> 100 /// 计算当使用标准箱子尺寸时主窗体客户区的尺寸 101 /// </summary> 102 /// <param name="statusBarHeight"> 状态条的高度 </param> 103 /// <returns> 客户区的尺寸 </returns> 104 public Size GetClientSize( int statusBarHeight) 105 { 106 int width = (Properties.Resources.PushBox24.Width / 8 ) * LevelSize.Width; 107 int height = Properties.Resources.PushBox24.Height * LevelSize.Height + statusBarHeight; 108 if (width < 240 ) width = 240 ; 109 if (height < 48 ) height = 48 ; 110 if (width > 1008 ) width = 1008 ; 111 if (height > 672 ) height = 672 ; 112 return new Size(width, height); 113 } 114 115 /**/ /// <summary> 116 /// 根据客户区尺寸,计算箱子的尺寸,并相应设定要显示的图形单元 117 /// </summary> 118 public void SetBoxInfo() 119 { 120 if (HasError) return ; 121 if (LevelSize.IsEmpty) return ; 122 int rX = clientSize.Width / LevelSize.Width; 123 int rY = clientSize.Height / LevelSize.Height; 124 int r = Math.Min(rX, rY); 125 if (r >= 24 ) img = Properties.Resources.PushBox24; 126 else if (r >= 20 ) img = Properties.Resources.PushBox20; 127 else if (r >= 16 ) img = Properties.Resources.PushBox16; 128 else img = Properties.Resources.PushBox12; 129 boxSize = new Size(img.Height, img.Width / 8 ); 130 } 131 132 /**/ /// <summary> 133 /// 装入配置文件 134 /// </summary> 135 public void LoadConfig() 136 { 137 if (HasError) return ; 138 try 139 { 140 cfg.LoadConfig(); 141 } 142 catch (Exception ex) 143 { 144 SetExceptionMessage(ex); 145 } 146 } 147 148 /**/ /// <summary> 149 /// 保存组信息到配置文件 150 /// </summary> 151 /// <param name="groups"> 组信息 </param> 152 public void SaveConfig( string [] groups) 153 { 154 if (HasError) return ; 155 try 156 { 157 cfg.SaveConfig(groups); 158 } 159 catch (Exception ex) 160 { 161 SetExceptionMessage(ex); 162 } 163 } 164 165 /**/ /// <summary> 166 /// 保存当前选项及当前走法到配置文件 167 /// </summary> 168 public void SaveConfig() 169 { 170 if (HasError) return ; 171 try 172 { 173 cfg.SaveConfig(stack.ToArray()); 174 } 175 catch (Exception ex) 176 { 177 SetExceptionMessage(ex); 178 } 179 } 180 181 /**/ /// <summary> 182 /// 装入当前组信息 183 /// </summary> 184 public void LoadGroup() 185 { 186 if (HasError) return ; 187 try 188 { 189 db.LoadGroup(Groups[Group]); 190 } 191 catch (Exception ex) 192 { 193 SetExceptionMessage(ex); 194 } 195 } 196 197 /**/ /// <summary> 198 /// 装入当前关信息 199 /// </summary> 200 public void LoadLevel() 201 { 202 active = Action.None; 203 if (HasError) return ; 204 try 205 { 206 db.LoadLevel(Level); 207 worker = db.Worker; 208 stack.Clear(); 209 pushSteps = 0 ; 210 isReplay = false ; 211 } 212 catch (Exception ex) 213 { 214 SetExceptionMessage(ex); 215 } 216 } 217 218 /**/ /// <summary> 219 /// 新建一关 220 /// </summary> 221 /// <param name="isCopy"> 是否复制当前关 </param> 222 /// <param name="size"> 新建关的尺寸 </param> 223 public void NewLevel( bool isCopy, Size size) 224 { 225 if (HasError) return ; 226 try 227 { 228 levelOem = Level; 229 Level = MaxLevel; 230 db.NewLevel(isCopy, size); 231 } 232 catch (Exception ex) 233 { 234 SetExceptionMessage(ex); 235 } 236 } 237 238 /**/ /// <summary> 239 /// 给出通关步骤 240 /// </summary> 241 /// <returns> 通关步骤 </returns> 242 public string GetSteps() 243 { 244 string steps = "" ; 245 if ( ! HasError) 246 { 247 try 248 { 249 steps = db.GetSteps(Level); 250 } 251 catch (Exception ex) 252 { 253 SetExceptionMessage(ex); 254 } 255 } 256 return steps; 257 } 258 259 /**/ /// <summary> 260 /// 记录通关步骤 261 /// </summary> 262 public void Record() 263 { 264 if (HasError) return ; 265 try 266 { 267 db.SaveLevel(Level, stack.ToArray(), pushSteps); 268 } 269 catch (Exception ex) 270 { 271 SetExceptionMessage(ex); 272 } 273 } 274 275 /**/ /// <summary> 276 /// 保存设计数据 277 /// </summary> 278 public void SaveDesign() 279 { 280 if (HasError) return ; 281 try 282 { 283 db.SaveDesign(active == Action.Create, Level); 284 } 285 catch (Exception ex) 286 { 287 SetExceptionMessage(ex); 288 } 289 } 290 291 /**/ /// <summary> 292 /// 删除最后一关 293 /// </summary> 294 public void DeleteLastLevel() 295 { 296 if (HasError) return ; 297 try 298 { 299 db.DeleteLastLevel(Level); 300 } 301 catch (Exception ex) 302 { 303 SetExceptionMessage(ex); 304 } 305 } 306 307 /**/ /// <summary> 308 /// 更新主窗体客户区 309 /// </summary> 310 /// <param name="dc"> 画布 </param> 311 /// <param name="rectangle"> 要在其中绘画的矩形 </param> 312 public void Draw(Graphics dc, Rectangle rectangle) 313 { 314 if (HasError) return ; 315 Rectangle box = PixelToBox(rectangle); 316 Rectangle box2 = new Rectangle(box.Left, box.Top, box.Width + 1 , box.Height + 1 ); 317 for ( int i = 1 ; i <= LevelSize.Height; i ++ ) 318 { 319 for ( int j = 1 ; j <= LevelSize.Width; j ++ ) 320 { 321 if ( ! box2.Contains(j, i)) continue ; 322 DrawBox(dc, j, i); 323 } 324 } 325 } 326 327 /**/ /// <summary> 328 /// 绘制一个单元格 329 /// </summary> 330 /// <param name="dc"> 画布 </param> 331 /// <param name="x"> 单元格的横坐标 </param> 332 /// <param name="y"> 单元格的纵坐标 </param> 333 void DrawBox(Graphics dc, int x, int y) 334 { 335 DrawBox(dc, db.Map[y, x], (x - 1 ) * boxSize.Width, (y - 1 ) * boxSize.Height); 336 } 337 338 /**/ /// <summary> 339 /// 绘制一个单元格 340 /// </summary> 341 /// <param name="dc"> 画布 </param> 342 /// <param name="idx"> 单元格的类型: 地 槽 墙 砖 箱子 工人 </param> 343 /// <param name="x"> 单元格的横坐标 </param> 344 /// <param name="y"> 单元格的纵坐标 </param> 345 void DrawBox(Graphics dc, int idx, int x, int y) 346 { 347 dc.DrawImage(img, x, y, new Rectangle(idx * boxSize.Width, 0 , boxSize.Width, boxSize.Height), GraphicsUnit.Pixel); 348 } 349 350 /**/ /// <summary> 351 /// 将单元格换算为像素 352 /// </summary> 353 /// <param name="box"> 单元格矩形 </param> 354 /// <returns> 像素矩形 </returns> 355 Rectangle BoxToPixel(Rectangle box) 356 { 357 return new Rectangle((box.Left - 1 ) * boxSize.Width, (box.Top - 1 ) * boxSize.Height, 358 (box.Width + 1 ) * boxSize.Width, (box.Height + 1 ) * boxSize.Height); 359 } 360 361 /**/ /// <summary> 362 /// 将像素换算为单元格 363 /// </summary> 364 /// <param name="pixel"> 像素矩形 </param> 365 /// <returns> 单元格矩形 </returns> 366 Rectangle PixelToBox(Rectangle pixel) 367 { 368 int x0 = pixel.Left / boxSize.Width + 1 ; 369 int y0 = pixel.Top / boxSize.Height + 1 ; 370 int x1 = (pixel.Right - 1 ) / boxSize.Width + 1 ; 371 int y1 = (pixel.Bottom - 1 ) / boxSize.Height + 1 ; 372 return new Rectangle(x0, y0, x1 - x0, y1 - y0); 373 } 374 375 /**/ /// <summary> 376 /// 根据指定的对角顶点创建矩形 377 /// </summary> 378 /// <param name="a"> 顶点 </param> 379 /// <param name="b"> 对角的顶点 </param> 380 /// <returns> 所需要的矩形 </returns> 381 Rectangle GetRectangle(Point a, Point b) 382 { 383 return Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y)); 384 } 385 386 /**/ /// <summary> 387 /// 设计模式下,当鼠标点击时要采取的动作 388 /// </summary> 389 /// <param name="invalid"> 输出:要重绘的区域 </param> 390 /// <returns> 是否发生动作 </returns> 391 public bool Design( out Rectangle invalid) 392 { 393 invalid = Rectangle.Empty; 394 Point to; 395 if ( ! ValidClick( out to)) return false ; 396 db.UpdateCounts(to.X, to.Y, false ); 397 Block.Update(ref db.Map[to.Y, to.X], pen); 398 db.UpdateCounts(to.X, to.Y, true ); 399 if (pen == Block.Man0 && HasWorker) pen = Block.Box0; 400 invalid = BoxToPixel(GetRectangle(to, to)); 401 return true ; 402 } 403 404 /**/ /// <summary> 405 /// 工人往指定方向前进一步(可能推着箱子) 406 /// </summary> 407 /// <param name="dir"> 前进的方向 </param> 408 /// <param name="isStop"> “撤销”时是否停留 </param> 409 /// <param name="invalid"> 输出:要重绘的区域 </param> 410 /// <returns> 是否成功 </returns> 411 public bool StepIt(Direction dir, bool isStop, out Rectangle invalid) 412 { 413 invalid = Rectangle.Empty; 414 if (HasError) return false ; 415 if (Direction.None == dir) return false ; 416 Point p1 = worker; // 工人前进方向一步的位置 417 Point p2 = worker; // 箱子前进方向一步的位置 418 switch (dir) 419 { 420 case Direction.East: p1.X ++ ; p2.X += 2 ; break ; 421 case Direction.South: p1.Y ++ ; p2.Y += 2 ; break ; 422 case Direction.West: p1.X -- ; p2.X -= 2 ; break ; 423 case Direction.North: p1.Y -- ; p2.Y -= 2 ; break ; 424 } 425 byte b1 = db.Map[p1.Y, p1.X]; // 工人前进方向一步位置上的东西 426 bool isBox = Block.IsBox(b1); // 是否推着箱子前进 427 if ( ! isBox && ! Block.IsBlank(b1)) return false ; // 如果没有推着箱子且前方不是空地则失败 428 if (isBox && ! Block.IsBlank(db.Map[p2.Y, p2.X])) return false ; // 如果推着箱子且箱子前方不是空地则失败 429 invalid = BoxToPixel(GetRectangle(worker, isBox ? p2 : p1)); // 要重绘的区域 430 stack.Push( new Step(dir, isBox, isStop)); // 记录走法步骤 431 Block.ManOut( ref db.Map[worker.Y, worker.X]); // 工人离开当前位置 432 Block.ManIn( ref db.Map[p1.Y, p1.X]); // 工人进入前方位置 433 if (isBox) 434 { 435 pushSteps++ ; // 更新推箱子步数 436 db.Boths += (db.Map[p2.Y, p2.X] - Block.Land) - (b1 - Block.Box0); // 更新已完成任务数 437 Block.BoxOut( ref db.Map[p1.Y, p1.X]); // 箱子离开当前位置 438 Block.BoxIn( ref db.Map[p2.Y, p2.X]); // 箱子进入前方位置 439 } 440 worker = p1; // 更新工人位置 441 return true ; // 工人成功前进一步(可能推着条子) 442 } 443 444 /**/ /// <summary> 445 /// 工人后退一步(可能连带箱子一起后退) 446 /// </summary> 447 /// <param name="invalid"> 输出:要重绘的区域 </param> 448 /// <returns> 是否完成“撤消” </returns> 449 public bool Back( out Rectangle invalid) 450 { 451 invalid = Rectangle.Empty; 452 if (HasError) return true ; 453 if (stack.Count == 0 ) return true ; 454 Step step = stack.Pop(); // 当前步骤 455 Point p1 = worker; // 工人后退方向一步的位置 456 Point p2 = worker; // 箱子的当前位置 457 switch (step.Direct) 458 { 459 case Direction.East: p1.X -- ; p2.X ++ ; break ; 460 case Direction.South: p1.Y -- ; p2.Y ++ ; break ; 461 case Direction.West: p1.X ++ ; p2.X -- ; break ; 462 case Direction.North: p1.Y ++ ; p2.Y -- ; break ; 463 } 464 invalid = BoxToPixel(GetRectangle(p1, step.IsBox ? p2 : worker)); // 要重绘的区域 465 Block.ManOut( ref db.Map[worker.Y, worker.X]); // 工人离开当前位置 466 Block.ManIn( ref db.Map[p1.Y, p1.X]); // 工人进入后退方向一步的位置 467 if (step.IsBox) 468 { 469 Block.BoxOut(ref db.Map[p2.Y, p2.X]); // 箱子离开当前位置 470 Block.BoxIn( ref db.Map[worker.Y, worker.X]); // 箱子进入工人原来的位置 471 db.Boths += (db.Map[worker.Y, worker.X] - Block.Box0) - (db.Map[p2.Y, p2.X] - Block.Land); // 更新已完成任务数 472 pushSteps -- ; // 更新推箱子步数 473 } 474 worker = p1; // 更新工人位置 475 return step.IsStop; // 是否完成“撤消” 476 } 477 478 /**/ /// <summary> 479 /// 寻找一条将工人移动到鼠标点击的位置的路线 480 /// </summary> 481 /// <returns> 移动的路线 </returns> 482 public Queue < Direction > GetMoveInfo() 483 { 484 Point to; 485 if ( ! CanTo( out to)) return null ; 486 return FindPath.Seek(db.Map, worker, to); 487 } 488 489 /**/ /// <summary> 490 /// 给出将箱子推动到鼠标点击的位置所需的信息 491 /// </summary> 492 /// <param name="dir"> 输出:工人移动的方向 </param> 493 /// <returns> 工人移动的步数 </returns> 494 public int GetPushInfo( out Direction dir) 495 { 496 dir = Direction.None; 497 if (HasError) return 0 ; 498 Point to; // 目的地 499 if ( ! CanTo( out to)) return 0 ; // 无效的目的地 500 if (to.Y != worker.Y && to.X != worker.X) return 0 ; // 目的地和工人不在同一条直线上 501 int z0 = (to.Y == worker.Y) ? worker.X : worker.Y; 502 int z9 = (to.Y == worker.Y) ? to.X : to.Y; 503 if (to.Y == worker.Y) dir = (z9 > z0) ? Direction.East : Direction.West; 504 else dir = (z9 > z0) ? Direction.South : Direction.North; 505 int i0 = Math.Min(z9, z0); 506 int i9 = Math.Max(z9, z0); 507 int steps = i9 - i0; // 目的地和工人之间的距离 508 int boxs = 0 ; 509 for ( int i = i0 + 1 ; i < i9; i ++ ) 510 { 511 byte bi = (to.Y == worker.Y) ? db.Map[worker.Y, i] : db.Map[i, worker.X]; 512 if (Block.IsBox(bi)) boxs ++ ; // 计算工人和目的地之间的箱子的个数 513 else if ( ! Block.IsBlank(bi)) boxs += 2 ; // “墙”和“砖”折算为两个箱子 514 } 515 if (boxs > 1 ) return 0 ; // 最多只能推着一个箱子前进 516 return steps - boxs; // 工人移动的步数 517 } 518 519 /**/ /// <summary> 520 /// 检查鼠标点击位置是否可达, 并将像素换算为单元格 521 /// </summary> 522 /// <param name="to"> 输出:换算后的位置 </param> 523 /// <returns> 是否可达 </returns> 524 bool CanTo( out Point to) 525 { 526 if ( ! ValidClick( out to)) return false ; 527 if ( ! Block.IsMan(db.Map[worker.Y, worker.X])) throw new Exception( " 内部错误:工人的位置上不是工人 " ); 528 if ( ! Block.IsBlank(db.Map[to.Y, to.X])) return false ; // 目的地必须是“地”或“槽” 529 if (to.Y == worker.Y && to.X == worker.X) return false ; // 目的地不能是工人当前的位置 530 return true ; // 目的地可达 531 } 532 533 /**/ /// <summary> 534 /// 检查鼠标点击位置是否有效, 并将像素换算为单元格 535 /// </summary> 536 /// <param name="to"> 输出:换算后的位置 </param> 537 /// <returns> 是否有效位置 </returns> 538 bool ValidClick( out Point to) 539 { 540 to = Point.Empty; 541 if (HasError) return false ; 542 to.Y = toPixel.Y / boxSize.Height + 1 ; 543 to.X = toPixel.X / boxSize.Width + 1 ; 544 if (toPixel.X >= boxSize.Width * LevelSize.Width || toPixel.Y >= boxSize.Height * LevelSize.Height) 545 return false ; // 目的地超出当前关的有效范围 546 return true ; // 目的地有效 547 } 548 } 549 }
550
上一篇:
使用 C# 开发智能手机软件:推箱子(十)
下一篇:
使用 C# 开发智能手机软件:推箱子(十二)
返回目录