-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathChapter_III-5.tex
1835 lines (1520 loc) · 75.5 KB
/
Chapter_III-5.tex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
\chapter{عمل تطبيقي: \textenglish{Mario Sokoban}}
المكتبة
\textenglish{SDL}
تقدّم، مثلما رأينا، عددا كبيرًا من الدوال الجاهزة للاستعمال. يمكن ألا نستطيع التعوّد عليها في البداية لقلّة التطبيق.
هذا العمل التطبيقي الأول في هذا الجزء من الكتاب سيعطيك فرصة التطبيق واختبار أشياء لم تسنح لك فرصة تجريبها.
أعتقد أنه بإمكانك التخمين، فهذه المرة لن يكون التطبيق عبارة عن كونسول وإنما سيحتوي على واجهة رسومية!
ماذا سيكون موضوع هذا العمل التطبيقي؟ لعبة
\textenglish{Sokoban}!\\
قد لا يعني لك هذا العنوان شيئًا، لكن هذه هي لعبة ذكاء تقليديّة. إنّها تنصّ على دفع صناديق لوضعها في أماكن محددة في متاهة.
\section{مواصفات \textenglish{Sokoban}}
\subsection{بخصوص \textenglish{Sokoban}}
الكلمة
"\textenglish{Sokoban}"
هي كلمة يابانية تعني "صاحب محلّ".\\
إنّها عبارة عن لعبة ذكاء تم اختراعها في الثمانينات بواسطة
\textenglish{Hiroyuki Imabayashi}.
و قد مثّلت برمجة هذه اللعبة تحدّيا كبيرًا في ذلك الزمن.
\subsubsection{الهدف من اللعبة}
المبدأ بسيط: تقوم بتحريك شخصية في متاهة. يجدر بالشخصية أن تقوم بدفع صناديق إلى مواقع محددة. لا يمكن للاعب أن يدفع صندوقين في آن واحد.
حتى وإن كان المبدأ مفهومًا وبسيطًا، فهذا لا يعني أن اللعبة في حدّ ذاتها سهلة! إذ أنه يجب عليك أحيانا تكسير رأسك بالتفكير لحلّ اللغز.
الصورة الموالية تُريك كيف تبدو اللعبة التي سنقوم ببرمجتها:
\begin{figure}[H]
\centering
\includegraphics[width=0.6\textwidth]{Chapter_III-5_Mario-Sokoban}
\end{figure}
\subsubsection{لماذا اخترتُ هذه اللعبة بالذات؟}
لأنها لعبة شعبية، جيدة لأن تكون موضوعًا برمجيًا ويمكننا إنشاؤها بواسطة ما تعلّمناه من الفصول السابقة.\\
يجب هنا أن نكون منظّمين. إذ أن الصعوبة لا تكمُن في برمجة اللعبة في حدّ ذاتها لكن في ما إن نظّمنا العمل. ولهذا فسنقوم بتقسيم البرنامج إلى عدّة ملفات
\InlineCode{.c}
بطريقة ذكيّة ونحاول إنشاء الدوال المناسبة.
من أجل هذا الأمر، قررت تغيير الطريقة بالنسبة لهذا العمل التطبيقي: لن أقدّم لك توجيهات وأقدّم التصحيح في النهاية. بالعكس، سأريك كيف نقوم ببناء المشروع كلّه من الألف إلى الياء.
\begin{question}
ماذا لو كنتُ أريد التدرّب لوحدي؟
\end{question}
حسنًا إذا فلتنطلق لوحدك، هذا أمر جيد!\\
ستحتاج ربّما وقتًا أكثر: لقد استغرقت شخصيا يومًا كاملًا لبرمجة اللعبة، هذا ليس بالوقت الكثير ربّما لأنه جرت العادة أن أقوم بالبرمجة وو أن أتحاشى الوقوع في بعض الفِخَاخ المتداولة (لكنّ هذا لم يمنعني من إعادة التفكير عدّة مرّات).
اِعلم بأنه توجد الكثير من الطرق التي يمكن بها برمجة هذه اللعبة. سأعطيك طريقتي في برمجتها: ليست أحسن طريقة ولكنها بالتأكيد ليست أسوء واحدة.\\
سننتهي من هذا التطبيق بقائمة من الاقتراحات لتحسين اللعبة، كما أنني سأعطيك الرابط لتحميل اللعبة والشفرة المصدرية الكاملة.
أنصحك مجددًا أن تحاول برمجة اللعبة لوحدك، حتى لو استغرقت 3 أو 4 أيام. اِفعل أحسن ما لديك. من المهم جدّا أن تقوم بالتطبيق.
\subsection{المواصفات}
المواصفات هي عبارة عن وثيقة نكتب فيها كل ما يجب على البرنامج أن يستطيع فعله.
هذا ما أقترحه:
\begin{itemize}
\item يجب أن يتمكن اللاعب من التحرّك في المتاهة ودفع الصناديق.
\item لا يمكنه أن يدفع صندوقين معًا.
\item تُربح الجولة إذا تواجدت كلّ الصناديق في الأماكن المخصصة لها.
\item سيتم حفظ كلّ مستويات اللعبة في ملف، (ليكن مثلا
\InlineCode{levels.lvl}).
\item يجب أن يتم دمج مـُنشئ المستويات
(\textenglish{Levels editor})
في البرنامج ليتمكن أي شخص كان من صنع مستويات خاصة به (هذا ليس أمرًا ضروريًا لكنه يعتبر إضافة مميزة!).
\end{itemize}
هذا كافٍ لنعمل كثيرًا.
يجب أن تعرف أنه هناك أشياء لا يجيد البرنامج القيام بها، ويجب ذِكرُ هذا الأمر أيضًا.
\begin{itemize}
\item برنامجنا قادر على التحكّم في مرحلة واحدة في المرّة الواحدة. إن أردت أن تكون اللعبة عبارة عن تتالي جولات، فما عليك سوى برمجة ذلك بنفسك في نهاية هذا العمل التطبيقي.
\item البرنامج لا يقوم بحساب الوقت المٌستغرق في كلّ جولة (نحن لا نجيد فعل ذلك بعد) ولا يمكنه حساب النقاط.
\end{itemize}
على أي حال، فكلّ الأشياء التي نريد القيام بها (خاصة مـُنشِئ المراحل) تأخذ منا وقتًا لابأس به.
\begin{information}
سأعطيك في نهاية العمل التطبيقي، جملة التحسينات التي تُمكن إضافتها إلى اللعبة. وهذه ليست كلمات في الهواء، لأنّها أفكار طبّقتها أنا شخصيّا في نسخة كاملة من اللعبة سأقترح عليك تنزيلها.\\
بالمقابل، لن أعطيك الشفرة المصدرية الخاصة بالنسخة الكاملة لأنني أريدك أن تعمل بنفسك وتتدرّب (لن أعطيك كلّ شيء على طبق من فضّة!).
\end{information}
\subsection{الحصول على الصور اللازمة لللعبة}
في معظم الألعاب ثنائية الأبعاد، أيّا كان نوعها، نسمّي الصور التي تشكّل اللعبة
\textenglish{\textit{Sprites}}.\\
في حالتنا، قرّرت إنشاء
\textenglish{Sokoban}
ووضع الشخصية
\textenglish{Mario}
لتكون اللاعب الرئيسي فيها (من هنا جاء اسم اللعبة
\textenglish{Mario Sokoban}).
بما أن
\textenglish{Mario}
شخصية لها شعبية كبيرة في عالم الألعاب
\textenglish{2D}،
لن نتعب في الحصول على الـ\textenglish{sprites}
الخاصّة بهذه الشخصيّة. سنحتاج أيضا إلى
\textenglish{sprites}
خاصة بالجدران، الصناديق، الأماكن المستهدفة، إلخ.
إذا بحثت في
\textenglish{Google}
عن
"\textenglish{sprites}"
فستحصل على عدّة نتائج. توجد العديد من المواقع التي توفّر
\textenglish{sprites}
خاصّة بألعاب
\textenglish{2D}
قد تكون لعبتها في السابق.
و هذه هي الّتي سنحتاج إليها:
\begin{Table}{2}
\textenglish{Sprite} & الشرح\\
\includegraphics{Chapter_III-5_Wall}&
جدار\\
\includegraphics{Chapter_III-5_Box}&
صندوق\\
\includegraphics{Chapter_III-5_Box2}&
صندوق متموضع فوق منطقة مستهدفة\\
\includegraphics{Chapter_III-5_Mario-down}&
بطل اللعبة
(\textenglish{Mario})
باتجاه الأسفل\\
\includegraphics{Chapter_III-5_Mario-right}&
بطل اللعبة باتجاه اليمين\\
\includegraphics{Chapter_III-5_Mario-left}&
بطل اللعبة باتجاه اليسار\\
\includegraphics{Chapter_III-5_Mario-up}&
بطل اللعبة باتجاه الأعلى\\
\end{Table}
الأسهل هو أن تقوم بتحميل الحزمة التي أعددتها لك.
\url{https://openclassrooms.com/uploads/fr/ftp/mateo21/sprites_mario_sokoban.zip}
\begin{information}
كان من الممكن أن أستعمل
\textenglish{sprite}
واحدًا خاصًا باللاعب. كان بإمكاني جعله موجّهًا إلى الأسفل فقط، لكن إضافة امكانية توجيهه في الاتجاهات الأربعة تضيف القليل من الواقعية. وهذا يشكّل تحدّيا آخر لنا!
\end{information}
قمت أيضًا بإنشاء صورة أخرى لتكون عبارة عن الواجهة الأساسية للعبة حين تبدأ، لقد أرفقت لك الصورة بالحزمة الّتي يفترض بك تنزيلها. لاحظ الصورة التالية:
\begin{figure}[H]
\centering
\includegraphics[width=0.5\textwidth]{Chapter_III-5_Home}
\end{figure}
ستلاحظ بأن الصور تأخذ صيغا مختلفة. يوجد منها ماهو
\textenglish{GIF}،
ماهو
\textenglish{PNG}
و حتى ماهو
\textenglish{JPEG}.
و لهذا فنحن بحاجة إلى استعمال المكتبة
\textenglish{SDL\_Image}.\\
فكّر في جعل مشروعك يعمل مع
\textenglish{SDL}
و
\textenglish{SDL\_Image}.
إذا نسيت كيف تفعل ذلك، فراجع الفصول السابقة. إذا لم تقم بتخصيص المشروع بشكل صحيح، سيشير المُترجم بأن الدوال التي تستعملها (مثل
\InlineCode{IMG\_Load})
غير موجودة!
\section{الدالة \texttt{main} والثوابت}
في كلّ مرة نبدأ بتحقيق مشروع مهمّ، من الواجب أن نقوم بتنظيم العمل في البداية.\\
بشكل عام، أبدأ في إنشاء ملف ثوابت
\InlineCode{constants.h}
إضافة إلى ملف
\InlineCode{main.c}
يحتوي الدالة
\InlineCode{main}
(فقط هذه الدالة). هذه ليست قاعدة لكنها طريقتي الخاصة في العمل، ولكلّ شخص طريقته الخاصة.
\subsection{ملفّات المشروع المختلفة}
أقترح أن نقوم بإنشاء ملفات المشروع كلّه الآن، (حتى وإن كانت فارغة في البداية). ها هي الملفات التي أنشئها إذا:
\begin{itemize}
\item \InlineCode{constants.h}:
تعريف الثوابت الشاملة الخاصة بكل البرنامج.
\item \InlineCode{main.c}:
الملف الذي يحتوي
\InlineCode{main}
(الدالة الرئيسية في البرنامج).
\item \InlineCode{game.c}:
الدوال الّتي تسيّر جولة من اللعبة
\textenglish{Sokoban}.
\item \InlineCode{game.h}:
نماذج الدوال الخاصة بالملف
\InlineCode{game.c}.
\item \InlineCode{editor.c}:
ملف يحتوي الدول التي تتحكم في مـُنشئ المستويات.
\item \InlineCode{editor.h}:
نماذج الدوال الخاصة بالملف
\InlineCode{editor.c}.
\item \InlineCode{files.c}:
الدوال الخاصّة بقراءة وكتابة ملفّات المستويات (مثل
\InlineCode{levels.lvl}).
\item نماذج الدوال الخاصة بالملف
\InlineCode{files.c}.
\end{itemize}
سنبدأ بإنشاء ملف الثوابت.
\subsection{الثوابت: \texttt{constants.h}}
هذا محتوى الملف
\InlineCode{constants.h}
الخاص بي:
\begin{Csource}
/*
constants.h
------------
By mateo21, for "Site du Zéro" (www.siteduzero.com)
Role : define some constants for all of the program (window size...)
*/
#ifndef DEF_CONSTANTS
#define DEF_CONSTANTS
#define BLOCK_SIZE 34 // Block size (square) in pixels
#define NB_BLOCKS_WIDTH 12
#define NB_BLOCKS_HEIGHT 12
#define WINDOW_WIDTH BLOCK_SIZE * NB_BLOCKS_WIDTH
#define WINDOW_HIGHT BLOCK_SIZE * NB_BLOCKS_HEIGHT
enum {UP, DOWN, LEFT, RIGHT};
enum {EMPTY, WALL, BOX, GOAL, MARIO, BOX_OK};
#endif
\end{Csource}
ستلاحظ الكثير من النقاط المهمّة في هذا الملف الصغير.
\begin{itemize}
\item يبدأ الملف بتعليق رأسي. أنصحك بوضع تعليق مماثل في كلّ ملفاتك (مهما كانت صيغتها
\InlineCode{.c}
أو
\InlineCode{.h}).
بشكل عام، التعليق الرأسي يحوي:
\begin{itemize}
\item اسم الملف،
\item اسم الكاتب (المبرمج)،
\item مهمّة الملف (أي فائدة الدوال الّتي يحويها)،
\item لم أقم بهذا هنا، لكن عادة يفترض أيضًا إضافة تاريخ كتابة الملف وتاريخ آخر تعديل عليه. هذا يسمح لك بإيجاد المعلومات بسرعة حينما تحتاج إليها وخاصة حينما يتعلّق الأمر بمشاريع كبيرة.
\end{itemize}
\item الملف محمّي ضد التضمينات غير المنتهية. لقد استعملت لذلك التقنية التي تعلّمناها في نهاية فصل المعالج القبلي. هنا، الحماية ليست مهمّة جدّا، لكن جرت العادة أن أستعملها في كلّ ملفاتي
\InlineCode{.h}
بدون استثناء.
\item أخيرًا، قلب الملف. ستجد لائحة من
\InlineCode{\#define}.
قمت بتحديد حجم كتلة بالبيكسل (كل
\textenglish{sprites}
هي عبارة عن مربّعات ذات حجم 34 بيكسل). أحدد بأن حجم النافذة يساوي 12*12 كتلة كعُرض. وبهذا أقوم بحساب أبعاد النافذة بعملية ضرب ثوابت بسيطة. ما أقوم به هنا ليس ضروريًا، لكنه يعود علينا بالفائدة: إذا أردت لاحقًا مراجعة حجم اللعبة، يكفي أن أقوم بتعديل هذا الملف وإعادة ترجمة المشروع فيعمل مع القيم الجديدة دون أية مشاكل.
\item أخيرًا، قمت بتعريف ثوابت عن طريق تعدادات غير معرّفة، الأمر مختلف قليلًا عمّا تعلّمناه في فصل إنشاء أنواع خاصة بنا. هنا أنا لست أقوم بتعريف نوع خاص بي بل أقوم فقط بتعريف ثوابت. هذا يشبه المعرّفات مع اختلاف بسيط: الحاسوب هو من يقوم بإعطاء عدد لكلّ قيمة (بدءً من 0). وبهذا يكون لدينا:
\InlineCode{UP} = 0،
\InlineCode{DOWN} = 1،
\InlineCode{LEFT} = 2،
إلخ. هذا ما سيسمح للشفرة بأن تكون مفهومة لاحقًا، سترى ذلك!
\end{itemize}
باختصار، لقد استعملت:
\begin{itemize}
\item معرّفات حينما أريد أن أعطي قيمة محددة لثابت (مثلًا 34 بيكسلًا).
\item تعدادات حينما تكون قيمة الثابت لا تهمّني. هنا، لا يهمني ما إن كانت القيمة المُرفقة بالعنصر
\InlineCode{UP}
هي 0 (كان من الممكن أن تكون 150، هذا لن يغيّر شيئا)، كلّ ما يهمّني هو أن يكون هذا العنصر مختلفا عن
\InlineCode{DOWN}
و
\InlineCode{LEFT}
و
\InlineCode{RIGHT}.
\end{itemize}
\subsubsection{تضمين تعريفات الثوابت}
المبدأ ينص على تضمين ملف الثوابت في كلّ الملفات
\InlineCode{.c}.\\
هكّذا، أستطيع استعمال الثوابت في أي مكان من الشفرة المصدرية الخاصة بالمشروع.
يعني أنه عليّ أن أكتب السطر التالي في كل بداية للملفات
\InlineCode{.c}:
\begin{Csource}
#include "constants.h"
\end{Csource}
\subsection{الدالة \texttt{main}: \texttt{main.c}}
الدالة الرئيسيّة الخاصة بالبرنامج سهلة جدًا. هي تقوم بإظهار واجهة اللعبة ثم التوجيه إلى القِسم المناسب.
\begin{Csource}
/*
main.c
------
By mateo21, for "Site du Zéro" (www.siteduzero.com)
Role : game menu. Allow to choose between the editor and the game.
*/
#include <stdlib.h>
#include <stdio.h>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include "constants.h"
#include "game.h"
#include "editor.h"
int main(int argc, char *argv[])
{
SDL_Surface *screen = NULL, *menu = NULL;
SDL_Rect menuPosition;
SDL_Event event;
int cont = 1;
SDL_Init(SDL_INIT_EMPTYO);
SDL_WM_SetIcon(IMG_Load("box.jpg"), NULL); // The icon must be loaded before SDL_SetVideoMode
screen = SDL_SetVideoMode(WINDOW_WIDTH, WINDOW_HIGHT, 32,SDL_HWSURFACE | SDL_DOUBLEBUF);
SDL_WM_SetCaption("Mario Sokoban", NULL);
menu = IMG_Load("menu.jpg");
menuPosition.x = 0;
menuPosition.y = 0;
while (cont)
{
SDL_WaitEvent(&event);
switch(event.type)
{
case SDL_QUIT:
cont = 0;
break;
case SDL_KEYDOWN:
switch(event.key.keysym.sym)
{
case SDLK_ESCAPE: // Want to quit the game
cont = 0;
break;
case SDLK_KP1: // Want to play
play(screen);
break;
case SDLK_KP2: // Want to edit levels
editor(screen);
break;
}
break;
}
// Cleaning the screen
SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 0, 0,0));
SDL_BlitSurface(menu, NULL, screen, &menuPosition);
SDL_Flip(screen);
}
SDL_FreeSurface(menu);
SDL_Quit();
return EXIT_SUCCESS;
}
\end{Csource}
الدالة
\InlineCode{main}
تتكفّل بتهيئة
\textenglish{SDL}،
و إعطاء عنوان للنافذة إضافة إلى منحها أيقونة. في نهاية الدالة، يتم استدعاء الدالة
\InlineCode{SDL\_Quit}
لإيقاف
\textenglish{SDL}
بشكل سليم.
الدالة تقوم بإظهار قائمة يتم تحميلها بواسطة الدالة
\InlineCode{IMG\_Load}
من المكتبة
\textenglish{SDL\_Image}.
\begin{Csource}
menu = IMG_Load("menu.jpg");
\end{Csource}
تلاحظ أنه، لكي أعطي أبعادًا للنافذة، أستعمل الثابتين
\InlineCode{WINDOW\_WIDTH}
و
\InlineCode{WINDOW\_HIGHT}
المعرّفين في الملف
\InlineCode{constants.h}.
\subsubsection{حلقة الأحداث}
الحلقة غير المنتهية تعالج الأحداث التالية:
\begin{itemize}
\item \textbf{إيقاف البرنامج}
(\InlineCode{SDL\_QUIT}):
إذا قمنا بطلب غلق البرنامج (النقر على العلامة
\InlineCode{X}
أعلى يمين النافذة) فسنعطي القيمة 0 للمتغير
\InlineCode{cont}
وتتوقف الحلقة. باختصار، هذا أمر تقليديّ.
\item \textbf{الضغط على الزر
\InlineCode{Escape}}:
إغلاق البرنامج (مثل
\InlineCode{SDL\_QUIT}).
\item \textbf{الضغط على الزر
\InlineCode{1}
من لوحة الأرقام}:
انطلاق تشغيل اللعبة (استدعاء الدالة
\InlineCode{play}).
\item \textbf{الضغط على الزر
\InlineCode{2}
من لوحة الأرقام}:
انطلاق تشغيل مُنشئ المراحل (استدعاء الدالة
\InlineCode{editor}).
\end{itemize}
كما ترى فالأمور تجري بسهولة تامة. إذا ضغطنا على الزر 1، يتم تشغيل اللعبة، ما إن تنتهي اللعبة، تنتهي الدالة
\InlineCode{play}
و نرجع لـ\InlineCode{main}
من أجل القيام بدورة أخرى للحلقة. الحلقة تستمر في الاشتغال مادمنا لم نطلب إيقاف البرنامج.
بفضل هذا التنظيم البسيط جدّا، يمكننا التحكم في الدالة
\InlineCode{main}
و ترك الدوال الأخرى (مثل
\InlineCode{play}
و
\InlineCode{editor})
تهتم بالتحكم في مختلف أجزاء اللعبة.
\section{اللعبة}
فلندخل إلى المرحلة الأكثر أهمية في الموضوع: الدالة
\InlineCode{play}!\\
هذه هي الدالة الأكثر أهمية في البرنامج، كن متيقّظًا لأن هذه الدالة هي حقّا ما يجدر بك فهمه. لأنك ستجد بعدها بأن مُنشئ المراحل ليس بالصعوبة التي تتخيّلها.
\subsection{المعاملات التي نبعثها للدالة}
الدالة
\InlineCode{play}
تحتاج إلى معامل واحد: المساحة
\InlineCode{screen}.
بالفعل، تم فتح النافذة في الدالة الرئيسية، ولكي تستطيع الدالة
\InlineCode{play}
أن ترسم على النافذة، يجب أن تقوم باسترجاع المؤشّر نحو المساحة
\InlineCode{screen}!
لو تقرأ مجددًا محتوى الدالة الرئيسية، ستجد بأنني قمت باستدعاء الدالة
\InlineCode{play}
و ذلك بإعطائها المؤشّر
\InlineCode{screen}:
\begin{Csource}
play(screen);
\end{Csource}
نموذج الدالة، الذي يمكنك وضعه في الملف
\InlineCode{game.h}،
هو التالي:
\begin{Csource}
void play(SDL_Surface* screen);
\end{Csource}
\begin{information}
الدالة لا تقوم بإرجاع أي شيء (ومن هنا الـ\InlineCode{void}).
يمكننا أن نجعلها إن أردنا تُرجع قيمة منطقية تشير إلى ما كنّا قد ربحنا الجولة أم لا.
\end{information}
\subsection{التصريح عن المتغيرات}
تحتاج هذه الدالة إلى كثير من المتغيرات.\\
لم أفكّر في كلّ المتغيرات التي أحتاجها من الوهلة الأولى. هناك من أضفتها لاحقًا وأنا أكتب الشفرة.
\subsubsection{متغيّرات من أنواع معرّفة في \textenglish{SDL}}
لكي نبدأ، ها هي كلّ المتغيرات ذات الأنواع المعرّفة في
\textenglish{SDL}
التي نحن بحاجة إليها:
\begin{Csource}
SDL_Surface *mario[4] = {NULL}; // 4 surfaces for the 4 directions of mario
SDL_Surface *wall = NULL, *box = NULL, *boxOK = NULL, *objective = NULL, *level= NULL, *currentMario = NULL;
SDL_Rect position, playerPosition;
SDL_Event event;
\end{Csource}
لقد قمتُ بإنشاء جدول من نوع
\InlineCode{SDL\_Surface}
يسمّى
\InlineCode{mario}.
و هو جدول من أربع خانات يقوم بتخزين
\textenglish{Mario}
في كلّ من الاتجاهات الأربعة (واحد للأسفل، الأعلى، اليمين واليسار).
توجد بعد ذلك العديد من المساحات الموافقة لكلّ من
\textenglish{sprites}
التي قمت بتحميلها أعلاه:
\InlineCode{wall}،
\InlineCode{box}،
\InlineCode{boxOK}
و
\InlineCode{objective}.
\begin{question}
بماذا ينفعنا
\InlineCode{currentMario}؟
\end{question}
هو عبارة عن مؤشّر نحو مساحة. وهو مؤشّر يؤشّر نحو المساحة الموافقة لـ\textenglish{Mario}
المتّجه نحو الاتجاه الحالي. أي أنه عبارة عن
\InlineCode{currentMario}
(\textenglish{Mario}
الحالي) الّذي سنقوم بتسويته في الشاشة. إذا رأيت في أسفل الدالة
\InlineCode{play}
ستجد:
\begin{Csource}
SDL_BlitSurface(currentMario, NULL, screen, &position);
\end{Csource}
لا نقوم إذا بلصق عنصر من الجدول
\InlineCode{mario}،
بل المؤشّر
\InlineCode{currentMario}.\\
و بلصق
\InlineCode{currentMario}،
يعني أننا سنلصق إما
\textenglish{Mario}
نحو الأسفل، أو نحو الأعلى، إلخ.\\
المؤشّر
\InlineCode{currentMario}
يؤشّر نحو إحدى خانات الجدول
\InlineCode{mario}.
ماذا بعد غير هذا؟\\
متغير
\InlineCode{position}
من نوع
\InlineCode{SDL\_Rect}
سنستعين به من أجل تعريف موضع العناصر التي سنقوم بتسويتها (سنحتاج إليها من أجل كلّ الـ\textenglish{sprites}،
و لا داعي لإنشاء
\InlineCode{SDL\_Rect}
من أجل كلّ مساحة!).
المتغير
\InlineCode{playerPosition}
مختلف: إنه يشير إلى أية خانة من الخريطة يوجد اللاعب. أخيرًا، المتغير
\InlineCode{event}
يهتم بتحليل الأحداث.
\subsubsection{متغيّرات "تقليديّة"}
حان الوقت لكي أعرّف متغيرات تقليديّة نوعًا ما من نوع
\InlineCode{int}:
\begin{Csource}
int cont = 1, remainingGoals = 0, i = 0, j = 0;
int map[NB_BLOCKS_WIDTH][NB_BLOCKS_HEIGHT] = {0};
\end{Csource}
\InlineCode{cont}
و
\InlineCode{remainingGoals}
هي متغيرات منطقية.\\
\InlineCode{i}
و
\InlineCode{j}
هي متغيرات مُساعِدة ستساعدنا في قراءة الجدول
\InlineCode{map}.
هنا تبدأ الأمور الهامّة حقّا. لقد قمت فعليًا بإنشاء جدول ذو بُعدين. لم أكلّمك عن هذا النوع من الجداول من قبل، لكنه الوقت المناسب لتتعلّم ما يعنيه. ليس الأمر صعبًا، سترى ذلك بنفسك.
لاحظ التعريف عن كثب:
\begin{Csource}
int map[NB_BLOCKS_WIDTH][NB_BLOCKS_HEIGHT] = {0};
\end{Csource}
هو عبارة عن جدول من
\InlineCode{int}
(أعداد صحيحة) يختلف في كونه يأخذ حاضنتين مربّعتين
\InlineCode{[ ]}.
إذا كنت تتذكر جيدًا الملف
\InlineCode{constants.h}،
فـ\InlineCode{NB\_BLOCKS\_WIDTH}
و
\InlineCode{NB\_BLOCKS\_HEIGHT}
هما ثابتان يأخذ كلاهما القيمة 12.
هذا الجدول سيتمّ إنشاءه في وقت الترجمة هكذا:
\begin{Csource}
int map[12][12] = {0};
\end{Csource}
\begin{question}
لكن، ماذا يعني هذا؟
\end{question}
هذا يعني أنه من أجل كلّ "خانة" من
\InlineCode{map}
توجد 12 خانة داخلية.\\
بهذا تكون لدينا المتغيرات التالية:
\begin{Console}
map[0][0]
map[0][1]
map[0][2]
map[0][3]
map[0][4]
map[0][5]
map[0][6]
map[0][7]
map[0][8]
map[0][9]
map[0][10]
map[0][11]
map[1][0]
map[1][1]
map[1][2]
map[1][3]
map[1][4]
map[1][5]
map[1][6]
map[1][7]
map[1][8]
map[1][9]
map[1][10]
...
map[11][2]
map[11][3]
map[11][4]
map[11][5]
map[11][6]
map[11][7]
map[11][8]
map[11][9]
map[11][10]
map[11][11]
\end{Console}
إذا هو جدول من 12 * 12 = 144 خانة!\\
كلّ من هذه الخانات تمثّل خانة في خريطة اللعبة.
الصورة التالية تعطيك فكرة كيف قُمنا بتمثيل الخريطة:
\begin{figure}[H]
\centering
\includegraphics[width=0.6\textwidth]{Chapter_III-5_Map}
\end{figure}
و بهذا فالخانة في أعلى اليسار مخزّنة في
\InlineCode{map[0][0]}.\\
الخانة أعلى اليمين مخزنة في
\InlineCode{map[0][11]}.\\
الخانة أسفل اليمين (آخر خانة) مخزنة في
\InlineCode{map[11][11]}.
حسب قيمة الخانة (والتي هي عدد صحيح)، نعرف أي خانة من النافذة تحتوي جدارًا، أو صندوقًا، أو منطقة مستهدفة، إلخ.\\
هنا بالضبط سنستفيد من تعريف التعداد السابق!
\begin{Csource}
enum {EMPTY, WALL, BOX, GOAL, MARIO, BOX_OK};
\end{Csource}
إذا كانت قيمة الخانة تساوي
\InlineCode{EMPTY} (0)
سنعرف بأن هذه المنطقة من الشاشة يجب أن تبقى بيضاء. إذا كانت تساوي
\InlineCode{WALL} (1)
فسنعرف أنه يجب أن نقوم بلصق صورة جدار، إلخ.
\subsection{تهيئات}
\subsubsection{تحميل المساحات}
و الآن، بما أننا قمنا بشرح كل متغيرات الدالة
\InlineCode{play}،
يمكننا البدء في القيام ببعض التهيئات:
\begin{Csource}
// Loading the sprites (Boxes, player...)
wall = IMG_Load("wall.jpg");
box = IMG_Load("box.jpg");
boxOK = IMG_Load("box_ok.jpg");
level = IMG_Load("level.png");
mario[DOWN] = IMG_Load("mario_bas.gif");
mario[LEFT] = IMG_Load("mario_gauche.gif");
mario[UP] = IMG_Load("mario_haut.gif");
mario[RIGHT] = IMG_Load("mario_droite.gif");
\end{Csource}
لا يوجد شيء صعب: نقوم بتحميل الكلّ بواسطة
\InlineCode{IMG\_Load}.\\
إن كانت هناك حالة خاصّة، فهي تحميل
\textenglish{Mario}.
إذ أننا نقوم بتحميل
\textenglish{Mario}
في كلّ من الاتّجاهات الأربعة في الجدول
\InlineCode{mario}
باستعمال الثوابت:
\InlineCode{UP}، \InlineCode{DOWN}، \InlineCode{LEFT}، \InlineCode{RIGHT}.
كوننا استعملنا هنا ثوابت فستصبح الشفرة أكثر وضوحًا -كما تلاحظ-. كان بإمكاننا استعمال
\InlineCode{mario[0]}،
لكن من الأفضل ومن الأكثر وضوحًا أن نستعمل
\InlineCode{mario[UP]}
مثلًا!
\subsubsection{التوجيه الابتدائيّ لـ\textenglish{Mario} (\texttt{currentMario})}
نهيّئ بعدها
\InlineCode{currentMario}
لكيّ تكون له وجهة ابتدائيّة:
\begin{Csource}
currentMario = mario[DOWN]; // Mario will be headed down when starting the program
\end{Csource}
وجدت أنه من المنطقي أكثر أن أبدأ المرحلة فيما يكون
\textenglish{Mario}
موجّها نحو الأسفل (أي نحونا)، كان بإمكانك أن تكتب مثلًا:
\begin{Csource}
currentMario = mario[RIGHT];
\end{Csource}
ستُلاحظ بأن
\textenglish{Mario}
سيكون موجّهًا نحو اليمين في بداية اللعبة.
\subsubsection{تحميل الخريطة}
الآن، يجدر بنا ملئ الجدول ثنائي الأبعاد
\InlineCode{map}.
لحدّ الآن، الجدول لا يحتوي إلا أصفارًا.\\
يجب أن نقرأ المستوى المخزّن في الملف
\InlineCode{levels.lvl}:
\begin{Csource}
// Loading the level
if (!loadLevel(map))
exit(EXIT_FAILURE); // We stop the game if we couldn't load the level
\end{Csource}
لقد اخترت معالجة تحميل (وحفظ) المستويات بواسطة دوال متواجدة بالملف
\InlineCode{files.c}.\\
هنا ، نستدعي إذا الدالة
\InlineCode{loadLevel}.
سنقوم بدراستها بالتفصيل لاحقا (هي ليست معقدة كثيرًا على أي حال). كل ما يهمنا هنا هو معرفة أنه تم تحميل المستوى في الجدول
\InlineCode{map}.
\begin{critical}
إذا لم يتم تحميل المستوى (لأن ملف
\InlineCode{levels.lvl}
غير موجود)، ستُرجع الدالة "خطأ". أمّا في الحالة المعاكسة فتُرجع "صحيح".
\end{critical}
نقوم إذا باختبار نتيجة التحميل بواسطة شرط. إذا كانت النتيجة سلبية (من هنا استعملت إشارة التعجّب لأعبّر عن ضدّ الشرط) يتوقف كلّ شيء: سنستدعي الدالة
\InlineCode{exit}.\\
في الحالة الأخرى، كلّ شيء يعمل بشكل جيّد إذًا ويمكننا المواصلة.
نحن نملك الآن جدولا
\InlineCode{map}
يصف محتوى كلّ خانة:
\InlineCode{WALL}، \InlineCode{EMPTY}، \InlineCode{BOX}\dots
\subsubsection{البحث عن وضعية الانطلاق لـ\textenglish{Mario}}
يجب الآن أن نعطي قيمة ابتدائية للمتغير
\InlineCode{playerPosition}.\\
هذا المتغير من نوع
\InlineCode{SDL\_Rect}
خاصّ قليلًا. لن نستعين به لتخزين الإحداثيات بالبيكسل وإنما بتخزينه بدلالة الـ"خانات" في الخريطة. وبهذا فإن كانت لدينا:
\InlineCode{playerPosition.x == 11}
و
\InlineCode{playerPosition.y == 11}
فهذا يعني أن اللاعب متواجد في آخر خانة في أسفل يمين الخريطة.\\
يمكنك الرجوع إلى الصورة السابقة لتتوضح لك الأمور أكثر.
سنقوم بالتقدّم داخل الجدول
\InlineCode{map}
و ذلك باستعمال حلقتين. نستعمل المتغير
\InlineCode{i}
للتقدّم في الجدول عموديًا، ونستعمل المتغير
\InlineCode{j}
للتقدّم فيه أفقيًا:
\begin{Csource}
// We search for the position of Mario in the beginning of the game
for (i = 0 ; i < NB_BLOCKS_WIDTH ; i++)
{
for (j = 0 ; j < NB_BLOCKS_HEIGHT ; j++)
{
if (map[i][j] == MARIO) // If Mario is in this position
{
playerPosition.x = i;
playerPosition.y = j;
map[i][j] = EMPTY;
}
}
}
\end{Csource}
في كلّ خانة، نختبر ما إن كانت هذه الأخيرة تحتوي
\InlineCode{MARIO}
(أي نقطة انطلاق اللاعب في الخريطة). إذا كانت كذلك، نقوم بتخزين الإحداثيات الحالية (المتواجدة في
\InlineCode{i}
و
\InlineCode{j})
في المتغير
\InlineCode{playerPosition}.\\
نمسح أيضًا الخانة وذلك بإعطائها القيمة
\InlineCode{EMPTY}
لكي يتم اعتبارها كخانة فارغة لاحقًا.
\subsubsection{تفعيل تكرار الضغط على الأزرار}
آخر شيء، أمر سهل جدًا: سنقوم بتفعيل تكرار الضغط على الأزرار لكي نستطيع التحرّك في الخريطة بترك الزر مضغوطًا.
\begin{Csource}
// Enabeling keys repetition
SDL_EnableKeyRepeat(100, 100);
\end{Csource}
\subsection{الحلقة الرئيسية}
حسنًا، لقد قُمنا بتهيئة كلّ شيء، يمكننا الآن العمل على الحلقة الرئيسية.
إنها حلقة تقليديّة تعمل بنفس المخطط الذي تعمل به الحلقات التي رأيناها لحدّ الآن. هي فقط كبيرة قليلًا.
فلنرى عن قرب \InlineCode{switch}
الذي يختبر الحدَث:
\begin{Csource}
switch(event.type)
{
case SDL_QUIT
cont = 0;
break;
case SDL_KEYDOWN:
switch(event.key.keysym.sym)
{
case SDLK_ESCAPE:
cont = 0;
break;
case SDLK_UP:
currentMario = mario[UP];
movePlayer(map, &playerPosition, UP);
break;
case SDLK_DOWN:
currentMario = mario[DOWN];
movePlayer(map, &playerPosition, DOWN);
break;
case SDLK_RIGHT:
currentMario = mario[RIGHT];
movePlayer(map, &playerPosition, RIGHT);
break;
case SDLK_LEFT:
currentMario = mario[LEFT];
movePlayer(map, &playerPosition, LEFT);
break;
}
break;
}
\end{Csource}
إذا ضغطنا على الزر
\InlineCode{Esc}،
فستنتهي اللعبة ونرجع للقائمة الرئيسية.
كما ترى، لا توجد العديد من الأحداث لنعالجها: سنختبر فقط ما إن ضغط اللاعب على الأزرار "أعلى"، "أسفل"،"يمين" أو "يسار" من لوحة المفاتيح.\\
على حسب الزر المضغوط نغيّر اتجاه
\textenglish{Mario}.
وهنا يتدخّل المتغير
\InlineCode{currentMario}!
إذا ضغطنا السهم الموجه نحو الأعلى إذًا:
\begin{Csource}
currentMario = mario[UP];
\end{Csource}
إذا ضغطنا على السهم الموجّه نحو الأسفل فإذًا:
\begin{Csource}
currentMario = mario[DOWN];
\end{Csource}
الآن، شيء مهمّ جدًا: نستدعي الدالة
\InlineCode{movePlayer}.
هذه الدالة ستقوم بتحريك اللاعب في الخريطة إن كان له الحق في فعل ذلك.
\begin{itemize}
\item مثلًا، لا يمكننا أن نُحرك
\textenglish{Mario}
إلى الأعلى إن كان متواجدًا أصلًا في الحافة العلوية للنافذة.
\item لا يمكننا أيضًا أن نحرّكه للأعلى إن كان فوقه جدار.
\item لا يمكننا أن نحرّكه للأعلى إن كان فوقه صندوقان.
\item على العكس، يمكننا تحريكه للأعلى إن تواجد صندوق واحد فوقه.
\item لكن احذر، لا يمكننا تحريكه للأعلى إن تواجد صندوق واحد فوقه وكان هذا الصندوق متواجد أصلًا في الحافة العلوية للنافذة!
\end{itemize}
\begin{question}
يا إلاهي! ما هذا السوق؟
\end{question}
هذا ما نسمّيه بـ\textbf{معالجة الاصطدامات}
(\textenglish{Collisions management}).
و لكي أضمن لك، نحن نقوم بالتعامل مع الاصطدامات البسيطة بما أن اللاعب يتحرّك خانة بخانة وفي أربع اتجاهات فقط. في لعبة ثنائية الأبعاد أين يتحرّك اللاعب في كلّ الاتجاهات بيكسلا ببيكسل، يكون التحكّم في الاصطدامات أمرا أصعب.
لكن هناك ماهو أسوء: الألعاب ثلاثية الأبعاد. التحكم في الاصطدامات في لعبة ثلاثية الأبعاد يُعدّ كابوسًا بالنسبة للمبرمجين. لحسن الحظ، توجد مكتبات للتحكّم في الاصطدامات في العوالم ثلاثية الأبعاد والتي تقوم بالكثير من العمل في مكاننا.
لنرجع للدالة
\InlineCode{movePlayer}
و لنركّز. نقوم بإعطائها ثلاثة معاملات:
\begin{itemize}
\item الخريطة: لكي تستطيع قراءتها وأيضًا التعديل عليها إذا قمنا بتحريك صندوق مثلًا.
\item وضعية اللاعب: هنا أيضًا، يجب على الدالة قراءة و"ربما" تعديل وضعية اللاعب.
\item الإتجاه الذي نطلب من اللاعب التوجّه إليه: نستعمل هنا أيضًا الثوابت:
\InlineCode{UP}، \InlineCode{DOWN}، \InlineCode{LEFT}، \InlineCode{RIGHT}
من أجل فهم الشفرة بشكل أفضل.
\end{itemize}
سندرس الدالة
\InlineCode{movePlayer}
لاحقًا. كان بإمكاني وضع كلّ الاختبارات داخل
\InlineCode{switch}،
لكن بهذا سيصبح كبيرًا وستصعب علينا قراءته. ومن هنا نرى الفائدة من تقسيم الشفرة إلى عدّة دوال.
\subsubsection{التسوية، فلنسوّي كلّ شيء}
لقد انتهينا من
\InlineCode{switch}:
في هذه الوضعية من البرنامج، قد تكون الخريطة قد تغيّرت وكذا وضعية اللاعب. مهما كان، لقد حان وقت التسوية!
سنبدأ بمسح الشاشة وذلك بإعطائها لون خلفية أبيض:
\begin{Csource}
// Clearing the screen
SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format, 255, 255, 255));
\end{Csource}
و الآن، نقوم بالتقدّم في الجدول ذو البعدين
\InlineCode{map}
لكي نعرف أي عنصر سنقوم بتسويته وفي أي منطقة من الشاشة.\\
سنستعمل حلقتين كما رأينا سابقًا للتقدّم في الـ144 خانة من الجدول:
\begin{Csource}
// Placing the objects on the screen
remainingGoals = 0;
for (i = 0 ; i < NB_BLOCKS_WIDTH ; i++)
{
for (j = 0 ; j < NB_BLOCKS_HEIGHT ; j++)
{
position.x = i * BLOCK_SIZE;
position.y = j * BLOCK_SIZE;
switch(map[i][j])
{
case WALL:
SDL_BlitSurface(wall, NULL, screen, &position);
break;
case BOX:
SDL_BlitSurface(box, NULL, screen, &position);
break;
case BOX_OK:
SDL_BlitSurface(boxOK, NULL, screen, &position);
break;
case GOAL:
SDL_BlitSurface(level, NULL, screen, &position);
remainingGoals = 1;
break;
}
}
}
\end{Csource}
من أجل كل خانة، نحضّر المتغير
\InlineCode{position}
(من نوع
\InlineCode{SDL\_Rect})
لكي نضع العنصر الحالي في المكان المناسب من الشاشة.\\
العملية جدّ بسيطة:
\begin{Csource}
position.x = i * BLOCK_SIZE;
position.y = j * BLOCK_SIZE;
\end{Csource}
يكفي ضرب
\InlineCode{i}
بـ\InlineCode{BLOCK\_SIZE}
لكي نعرف قيمة
\InlineCode{position.x}.\\
و بهذا، فإن كنا نتواجد في الخانة الثالثة، أي أن
\InlineCode{i} = 2
(لا تنس أن
\InlineCode{i}
يبدأ من 0!)، نقوم إذًا بالعملية
\mbox{2 * 34 = 68}.