יום ראשון, 25 בדצמבר 2011

ללמוד מחיפושיות



"באגים הם חשובים"








כשלמדתי מדעי המחשב, לפני... כמה וכמה שנים, היה לי מורה לתכנות בשם אורי מוסנזון. הוא היה מורה מצוין מסיבות רבות, ובכלל אחלה בן אדם באופן כללי.


והוא לימד אותי דבר חשוב מאוד על באגים. הוא לימד אותי שבאגים הם חשובים. 


בתור מתכנתים אנחנו עושים הכל כדי להמנע מבאגים, לא בשונה הרבה מג'וקים, הינו שמחים לגלות כלפיהם אלימות פיזית חסרת רחמים אם רק יכולנו. הם תמיד מפריעים לעבודה, משגעים אותנו, מבזבזים לנו את הזמן, ומעלים אצלנו שאלות קיומיות בסגנון "למה לא הלכתי להיות אומן...". 


הרבה אנשים עוזבים את מקצוע התכנות, בגלל התסכול חסר הרחמים, שהם חוו בעיקבות באגים.   באגים גורמים לאנשים לחשוב שהמחשב שונא אותם. לפעמים הם אפילו משכנעים אותנו שוודו זה דבר אמיתי, שהמחשב מקולל. 




"יש דרכים טובות מאוד להתמודד עם באגים. ולא - לשבור את המסך עם המקלדת היא לא אחת מהן."




הייתי רוצה להגיד לכם שזה לא נכון, שבאגים הם תמיד בסופו של דבר הגיוניים, ושתמיד אפשר לפתור אותם, עם קצת רצון ומאמץ. אבל האמת שאני עוד לא לגמרי משוכנע בזה, והייתי מעדיף לא לעצבן את אלוהי הביטים אם אני לא חייב.


ובכול זאת, יש דרכים טובות מאוד להתמודד עם באגים. ולא - לשבור את המסך עם המקלדת היא לא אחת מהן. הדרך להתמודד בצורה טובה עם באגים, מחזירה אותנו למשפט שהזכרנו קודם "באגים הם חשובים"




באגים הם סימן מאלוהי הביטים, שיש לנו משהו ללמוד.












אז מה הכוונה בזה שבאגים הם חשובים. הכוונה היא, שבאגים הם לא איזה תוצר לוואי סתמי של טעות בתכנות, שכדאי לדלג עליו כמה שיותר מהר ולהמשיך הלאה. וזאת ברגע שמצאנו פתרון.
זה הרבה פעמים נראה ככה, שטעות כתיב, משתנה לא נכון, תנאי שלא נכתב כמו שצריך, הם סתם איזה משהו שעשינו בחוסר תשומת לב, ושברגע שעלינו עליהם (או מיד אחרי שקראנו לעצמנו מטומטם או אדיוט) אז זה הזמן להמשיך הלאה ו"להתקדם בעבודה". 


באגים הם סימן מאלוהי הביטים, שיש לנו משהו ללמוד.
כמו בהרבה מקומות אחרים בחיים, ניסיון לעשות משהו שלא הצליח הוא שיעור. הוא לא כישלון. הוא הופך להיות כישלון רק אם התיאשנו והפסקנו. לפי זה אולי תטעו לחשוב, שזה שאתם פותרים באג, זה אומר שניסיתם וניסיתם, ואז הצלחתם - אז הכל בסדר. אבל ההצלחה האמיתית מגיעה, (בתקווה שהיא באמת מגיעה) רק בפעם הבאה שתתקלו באותו באג או בבאג דומה.


מה זה אומר? ניתן דוגמה פשוטה. 


יש שני סוגי באגים עיקריים: באגים של סינטק (טעויות בכתיבת הקוד שהקומפיילר מוצא בקלות) ובאגים לוגיים, שהתוכנית מתקמפלת ואולי אפילו עובדת, עד שמגיעים לנקודה שמשהו לא מתרחש כמו שצריך. אז בדוגמה שלנו, נסתכל על טעות מסוג כתיבה. נגיד שכתבנו שורה בסגנון:


echo "Hello World \n;

"מה יקרה בפעם הבאה שאני שתקרה לי אותה טעות?"



גם בלי קומפילר, יש מצב שכבר שמתם לב שחברים לי גרשיים בסוף הפקודה " ושבעצם לא סיימתי את הסטרינג. זו טעות שדי קל לגלות, וקרוב לודאי שהקומפיילר יצעק עליה. אז סבבה, אני אקבל הודעת שגיאה, אלך למקום הזה ואתקן- וזהו. זה מהיר זה קל, וזה לא מבזבז זמן יותר מידי. מה יקרה בפעם הבאה שאני שתקרה לי אותה טעות? קרוב לודאי שאותו תהליך, ושוב זה לא נורא, כי זה לא ממש מבזבז זמן. פשוט רואים מה הטעות, ויש יודעים אייך לתקן.

הבעיה היא, מה קורה אם אין לנו את המידע הזה. ננייח שזו טעות, שהקומפילר לא אומר עליה כלום, הוא פשוט לא ידפיס את השורה או משהו. אז יכול להיות שנצטרך לעשות איזהו שהוא תהליך מחשבתי, ומעבר בקוד כדי למצוא את הטעות הזאת. ויכול להיות שז במקום שתי דקות יקח לנו חצי שעה למצוא את הבעיה. 
וכאן הגענו לנקודה החשובה. בפעם הראשונה לא היתה לנו ברירה, היינו צריכים לחפש ולמצוא את הבאג. את החצי שעה הזאת לא נוכל להחזיר לנו, אבל אם הצלחנו ללמוד משהו מהפעם הראשונה, שיעזור לנו לבזבז פחות זמן בפעם השנייה או השלישית וכן הלאה, אז למדנו וחסכנו זמן יקר.



תבדקו את ההנחות שלכם



זו בדיוק הסיבה למה עלינו להתיחס לבאגים כחשובים. בעיקבות מה שאורי לימד אותי, גם בעבודה שלי, וגם כשאני מכשיר או מיעץ למתכנתים מתחילים, אני תמיד מדגיש ומתיחס ברצינות לנקודה הזאת. צריך ללמוד את הבאגים שלנו. צריך להבין אייך הם קרו, למה עשינו את הטעות שגרמה להם מלכתחילה, מה עזר לנו לפתור אותם, ואייך נוכל לזהות ולפתור אותם יותר מהר בעתיד. 

מצורת הלמידה הזאת של באגים, לומדים אחר כך עוד כמה כללי אצבע שעוזרים למצוא באגים. אחד מהם הוא למשל 

תבדקו את ההנחות שלכם

הרבה פעמים אנחנו עושים הנחות לגבי קוד שלנו, ואז אנחנו מחפשים, מחפשים, ומחפשים ובסוף מגלים שעשינו - במקום + או משהו. לפעמים אולי נחשוב שפונקציה לא עובדת, ויסתבר שלמרות שהיינו בטוחים שקראנו לה, פשוט לא קראנו לה. אז פשוט - תבדקו את ההנחות שלכם.

כלל נוסף:

הקוד האחרון ששינינו הוא בדרך כלל מקור הטעות.

 יש מקרים אחרים כמובן, של שילובי קוד שיצרו תופעה חדשה, או משהו שפיספסנו. אבל בחלק גדול מאוד של המקרים, הדבר האחרון שנגענו בו מאז הקוד שעבד טוב, הוא זה שגורם את הבעיה.
 למה זה חשוב? במיוחד בבאגים לוגים, ובמיוחד במערכות מרכבות, או משחקים מורכבים, יש מצבים ששינוי קטן, טעות קטנה, יגרמו לתופעות, שלא הייתם מנחשים אפילו. דברים שלא יכולתם לדמיין. הם יכולים לגרום לכם לקפוץ לכל מיני חלקים בקוד, להתחיל לעשות שינויים דראסטיים, או כל מיני walkaround שרק יוסיפו בעיות במקום לעזור לכם להפתר מהבעיות הקימות. זה יכול להגיע למצב, שפתאום תגלו את הבאג, אבל אז תצטרכו להתמודד עם כל מיני שינויים שעשיתם. 
אז רגע, תעצרו את הסוסים, תחשבו על מה עשיתם מאז הפעם האחרונה שהקוד עבד, במה נגעתם. ואייך משהו שעשיתם שם, יכל להשפיע בצורה דראסטית גם על חלקים אחרים. 

זה לא משהו שאני עשיתי בקוד. (וזה כבר טוב :) ) 





מקרה שקרה. מתכנת שעבדתי על פרויקט שלו כשהוא היה בחופש, התקשר אלי כעבור כמה זמן, ואמר לי שהוא לא מצליח לעשות publish. הוא לא היה בטוח אם זה משהו שהוא עשה בקוד, שינוי גירסה של התוכנה שהוא עבד עליה, או שזה  היו שינויים שאני עשיתי בקוד. חשבתי על זה קצת, ואז אמרתי - "רגע, מאז שחזרת, עשית איזשהו publish שהצליח?". התשובה היתה חיובית, אז כבר צימצמנו את הבעיה לזה, שזה לא משהו שאני עשיתי בקוד. (זה כבר טוב :) ) 

עכשיו נותרה השאלה, האם זה משהו שהוא עשה בקוד, או שינוי גירסה. אז אמרתי לו לנסות לעשות publish  לגירסה ישנה של הקוד, שהוא יודע שהוא הצליח לעשות לה publish בעבר. ברגע שהוא ניסה ולא הצליח, אז כבר היה ברור שזה משהו שנגרם מהעידכון תוכנה, ומשם הדרך לפתרון היתה יותר קלה ומהירה. 

בואו נתן דוגמה לבאג, ונראה אייך אפשר ללמוד ממנו, על מנת למנע ממנו.

var  enemyA:Enemy = new Enemy();
enemyA.x = enemyPositions[1].x;
enemyA.y = enemyPositions[1].y;
this.addChild(enemyA);

var  enemyB:Enemy = new Enemy();
enemyB.x = enemyPositions[1].x;
enemyB.y = enemyPositions[1].y;
this.addChild(enemyB);

זה לא הקוד הכי מבריק בעולם, אבל הוא יספיק לנו.
נניח שאני מריץ את המשחק, ורואה שבמקום שני אויבים, אני רואה רק אחד מהם. רק את B - האויב השני. אני חושב שאולי לא הוספתי את הראשון למסך, אז אני בודק ורואה שיש את השורה:

this.addChild(enemyA);

אז אני חושב שאולי לא מיקמתי אותו טוב במסך, אבל אני בודק את המיקום: 

enemyA.x = enemyPositions[1].x;
enemyA.y = enemyPositions[1].y;

וזה נראה טוב. 



זה באמת קשור לשכפול




אז אני חושב אולי כששיכפלתי את הקוד, שכחתי להחליף איפה שהוא את enemyA ל enemyB אבל אני בודק ורואה שהחלפתי בכל המקומות הנכונים.

בסופו של דבר אני מבין שזה באמת קשור לשכפול קוד כייון שאולי שיניתי את כל ה enemyA ל enemyB איפה שצריך, אבל לא החלפתי את האינדקס של המערך מ 1 ל 2, ולכן מה שקרה הוא בעצם שמיקמתי את אויב B בדיוק מעל A ולכן זה נראה ש A לא נמצא.
יכול להיות שגיליתי את זה בגלל שהדפסתי את המיקום של האויבים על המסך, וגיליתי שהם בדיוק באותו מקום (ואולי לא שאחד איכשהו מחוץ למסך כי לא חשדתי).
יכול להיות גם שחשבתי שכשהכנסתי את המיקומים למערך, אז עשיתי copy paste והמיקומים של שני האויבים הם זהים, ורק אחרי שבדקתי גיליתי שהטעות היא לא בנתונים, אלה בזה שאני לוקח את הנתון הלא נכון.

אז קודם כל, אנחנו כבר רואים אייך כללי האצבע עזרו לנו למצוא את הבעיה, לא הנחתי שבטוח מיקמתי את הדברים נכון ובדקתי, והתמקדתי בקוד האחרון ששיניתי. אז זו לא היתה כזו בעיה למצוא את זה יכול להיות שזה לקח לי 10 דקות רבע שעה, ואם הייתי נוהג אחרת זה היה לוקח יותר. 



"מה אני יכול לעשות בעתיד כדי להמנע מאותה טעות."




אבל פה כאמור זה לא נגמר, מה אפשר ללמוד מזה:

1. שכפול קוד - מקור לטעויות, כדאי לשים לב שעדכנו את מה שצריך לעדכן.
2. אם לא רואים משהו, זה לא אומר שהוא לא על המסך או שלא הוספנו אותו. יכול להיות שהוא מוסתר.
3. טעות יכולה להיות או בנתונים המקורים, או שאולי אני לוקח את הנתון הלא נכון
4. הדפסות של מספרים שלא מתנהגים כמו שצריך (במקרה הזה המיקום) יכולות לעזור למצוא את הבעיה.

אז אחרי שהבנתי מה בדיוק מקור הטעות, ושמתי לב מה עזר לי לפתור אותו, וחשבתי על מה אני יכול לעשות בעתיד כדי להמנע ממנה. אז אם יהיה לי מקרה דומה שיחזור על עצמו - שבו הוספתי משהו למסך, ואני לא רואה אותו. אני אוכל לצפות שאולי זו הבעיה, אני אוכל להשתמש בכלים האלה, וברעיונות האלה כדי לבדוק אם זו הבעיה ואם אני פותר אותה. ואם זו תהיה בעיה דומה, אני אחסוך תסכול מיותר, וזמן יקר.

אבל בזה זה לא נגמר, כי אותם כלים ואותה הסתכלות, יוכלו גם לעזור לי במקרים שונים לגמרי. נגיד הדפסתי טקסט ממסמך, וקיבלתי שתי שורות זהות. זו בעיה בסגנון שונה קצת, אבל ההתנהגות היא דומה. כי כמו המקרה שעברנו לב, שגילינו שהמיקומים זהים. אז פה כבר אנחנו יודעים שהנתונים זהים, אז אם נבדוק באופן דומה, נגלה אולי שבמסמך, שיכפלנו בטעות שורה, או שלא תיקנו שורה ששכפלנו, ויש שתי שורות זהות.
וכך פתרון בעיה מסוג אחד עוזר לפתור בעיה מסוג אחר.

בעיה אחרת שנתקלתי בה. יצרתי כלי לעיצוב שלבים, שהשתמשתי בו כדי לסדר מלבנים בצורות שונות. אז סידרתי מלבנים כך שתצא צורה של לב למשל. אבל כשניגשתי למשחק ראיתי שאני רואה פחות מלבנים ממה ששמתי ושזה לא מסודר כמו שצריך. בגלל שנתקלתי במקרה דומה בעבר, היסקתי שהמספרים ממנוע הבניה לא עברו כמו שצריך, כי השתמשתי במקום מסוים ב float ובמקום אחר קיבלתי אותו כ int מה שגרם לכך שהמיספרים עוגלו, והמלבנים "התישרו" למיקומים מסויימים. וכך הידע הקודם, והאינטואיציה שנבעה ממנו חסכו לי זמן רק.

אז המסר של הפוסט הזה הוא פשוט - הבאגים שלכם חשובים, תלמדו אותם, תלמדו מהם, זה יחסוך לכם זמן ותסכול, ויהפוך אתכם למתכנתים הרבה יותר טובים. 



ולסיום - פינת הסחטיין
רציתי לתת ח"ח עצבני לכל מי שהיה קשור ופעל למען האי-כנס של GameIS שהיה בשפיים. היה היה מעולה. אני אציין בפרט את מדיה גיים שהשאילה לנו את הציוד לפינת המשחקים, וכמובן את בנק הפועלים שאירחו אותנו במתחם ההכשרה שלהם. 





יום ראשון, 18 בדצמבר 2011

חזרזיר וציפור כועסת נכנסים לבר... (פיזיקה במשחקים חלק 3)



אני ממשיך את הדיון שהתחלתי בשני הפוסטים הקודמים על פיזיקה במשחקים. 


כפי שאמרתי בסוף הפוסט האחרון, נשאר לנו לעבור קצת על מה קורה ב MyContactListener ובפונקציה CheckAndHandleCollision שמטפלת בהתנגשויות


נתחיל ב MyContactListener

public class MyContactListener extends b2ContactListener{

var _contacts:Array;

public function MyContactListener() {
// constructor code
_contacts = [];
}
override public function BeginContact(contact:b2Contact):void{
_contacts.push(contact);
}
override public function EndContact(contact:b2Contact):void {
if(_contacts.indexOf(contact) >=0){
// removing contact
_contacts.splice(_contacts.indexOf(contact),1);
}
}
override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void{ 
var fixtureA:b2Fixture=contact.GetFixtureA();
var fixtureB:b2Fixture=contact.GetFixtureB();
var nameA:String = MovieClip(fixtureA.GetUserData()).name;
var nameB:String = MovieClip(fixtureB.GetUserData()).name;
if((nameA == "DragonBlinking" &&
                                     nameB.length >=5 && nameB.substr(0,5) == "enemy") ||
  (nameB == "DragonBlinking"&& 
                                    nameA.length >=5 && nameA.substr(0,5) == "enemy")){
//trace("disablingContact");
contact.SetEnabled(false);
}

}

}

כפי שאתם רואים הקוד הוא די קצר, ואפילו אפשר להגיד שיש בו דברים מיותרים.

הדבר הכי חשוב להבין הוא ש MyContactListener יורש מ b2ContactListener וזאת בגלל ש b2ContactListener בעצם כבר עושה את כל הטיפולים בהתנגשות מבחינת העולם הפיזיקלי.

מה זה אומר - בעצם כלום, כי b2ContactListener באמת לא עושה כלום. b2ContactListener היא רק מחלקה שמגדירה 4 פונקציות ולא מממשת אותן, כל זאת בשביל שאנחנו נוכל להגדיר אותן לשימוש שלנו. הפונקציות הן:

 BeginContact - נקראת כאשר נוצר מגע בן שני אוביקטים
EndContact - נקראת כאשר נפסק מדע בין שני אוביקטים
PreSolve - נקראת רגע לפני שנוצא המגע (ובעצם מאפשרת לנו לבטל אותו, כולל השפעותיו הפיזיקאליות
PostSolve - נקראת רגע אחרי שנוצר המגע (לכן מאפשרת שימוש בידע על הכוחות שפועלים על האוביקטים בעקבות המגע.)

את הפונקציות האלה, אנחנו לא חייבים להגדיר מחדש, אבל מן הסתם אם אנחנו רוצים להשתמש במנגנונים של box2d כדי לדעת מתי יש מגע בין אוביקטים ולטפל בהם, אנחנו צריכים להגדיר אותן.
אז בואו נראה את ההגדרה:

var _contacts:Array;

public function MyContactListener() {
// constructor code
_contacts = [];
}
override public function BeginContact(contact:b2Contact):void{
_contacts.push(contact);
}
override public function EndContact(contact:b2Contact):void {
if(_contacts.indexOf(contact) >=0){
// removing contact
_contacts.splice(_contacts.indexOf(contact),1);
}
}

אני עושה כאן דבר פשוט, כל פעם ש box2d מודיע לי על קשר חדש (על מגע חדש) שנוצר, אני מוסיף אותו למערך משלי, וכל פעם שהוא מסיר אותו אז אני מסיר אותו אצלי. 

למה? - כי למערך הזה יש לי גישה ברורה, ובכל רגע במשחק יש לי אפשרות לדעת בדיוק מה נוגע במה, ולהשתמש במידע הזה לצרכים שלי (נראה את זה בהמשך, כשנסתכל על הטיפול שלנו בהתנגשויות) 

נמשיך לפונקציה הבאה:

override public function PreSolve(contact:b2Contact, oldManifold:b2Manifold):void{ 
var fixtureA:b2Fixture=contact.GetFixtureA();
var fixtureB:b2Fixture=contact.GetFixtureB();
var nameA:String = MovieClip(fixtureA.GetUserData()).name;
var nameB:String = MovieClip(fixtureB.GetUserData()).name;
if((nameA == "PlayerBlinking" &&
                                     nameB.length >=5 && nameB.substr(0,5) == "enemy") ||
   (nameB == "PlayerBlinking"&& 
                                    nameA.length >=5 && nameA.substr(0,5) == "enemy")){
//trace("disablingContact");
contact.SetEnabled(false);
}

}

בתחלס ברוב המקרים לא הייתי עושה מימוש לפונקציה הזאת. הסיבה היא פשוטה. הפונקציה הזאת בעיקר משמשת אותי, כשאני רוצה לבדוק ממש לפני המגע אם הוא מתקיים, ולהחליט אם אני רוצה להשאיר אותו או לא. 

זה לא שימושי כמו שזה נשמע, כיוון שאנחנו כבר יודעים שיש לנו דרכים לעשות שבאוביקט מסוים, שום דבר לא יוכל לפגוע, על ידי הגדרתו כ sensor. או שהוא לא יתקל באוביקטים מסוימים, על ידי הגדרות של category bits ו mask bits). כיוון שאת הפרמטרים האלה ניתן לשנות בכל זמן, זה כבר נותן לנו שליטה רבה על מנגנון ההתנגשויות.

אבל כשחשוב לנו בזמן מסויים, ובנקודה מסוימת להחליט אם להתעלם ממגע שנוצר, אז זה המקום לעשות את זה. במקרה שמוצג בקוד למעלה PlayerBlinking זה שם שאני נותן ל Sprite של השחקן, אחרי שהוא נפגע, איבד פסילה, ואני לא רוצה שהוא יהיה פגיע מאויבים בזמן הקרוב. אז אני בודק שזה באמת PlayerBlinking שנוגע ב enemy מסוג כלשהו לפני שאני "מבטל" את המגע הזה.

מה שחשוב לשים לב אליו, הוא אייך לוקחים את המידע מה Fixtures של ה contact על מנת לבדוק מה נגע במה, אנחנו נראה שימוש דומה, בטיפול שנעה בהתנגשויות, שזה החלק הבא.

אם כן סיימנו עם ה MyContactListener בואו נסתכל על ה checkAndHandleCollisions. להזכירכם זוהי פונקציה שנקראת בכל צעד של המשחק, ומטרתה להוציא מידע מהמנוע של box2d ולעשות החלטות בהתאם. הינה היא בצורה מצומצמת שלה עם המידע הרלוונטי לנו:



// checkAndHandleCollisions  -------------------------
 
public function checkAndHandleCollisions():void{
// going over all the contacts
for each(var contact:b2Contact in PhysicsVars._contactListener._contacts){
if(contact.IsEnabled()==false){
continue;
}
var fixA:b2Fixture = contact.GetFixtureA();
var fixB:b2Fixture = contact.GetFixtureB();
var nameA:String = fixA.GetUserData().name;
var nameB:String = fixB.GetUserData().name;
var sprA:MovieClip = fixA.GetUserData();
var sprB:MovieClip = fixB.GetUserData();

...

// 20.8 fire touches enemies body
 if((isFireName(nameA) && isEnemyBodyName(nameB)) ||
(isFireName(nameB) && isEnemyBodyName(nameA))){
var eName2:String;
if(isFireName(nameA)){
eName2 = nameB;
}
else{
eName2 = nameA;
}
var tmpEnemy2:EnemyActor = _enemiesManager.getEnemyByName(eName2);
if(tmpEnemy2 && tmpEnemy2._isObstical == false){
tmpEnemy2.handleGotHit(true);
}
 
}
}
...
}

כיוון שמרבית הקוד בפונקציה הזאת, הוא ספציפי מידי מצד אחד, ובכל מקרה חוזר על עצמו מצד שני, השארתי רק את הדוגמה לתנאי if אחד, משורה די ארוכה של תנאים. אבל למעשה, באמת שבכל מקרה כולם מתנהגים אותו דבר.

נתחיל בקוד שמשתמש במידע מה ContcactListener

for each(var contact:b2Contact in PhysicsVars._contactListener._contacts){
if(contact.IsEnabled()==false){
continue;
}
var fixA:b2Fixture = contact.GetFixtureA();
var fixB:b2Fixture = contact.GetFixtureB();
var nameA:String = fixA.GetUserData().name;
var nameB:String = fixB.GetUserData().name;
var sprA:MovieClip = fixA.GetUserData();
var sprB:MovieClip = fixB.GetUserData();

אז אנחנו רואים שאנחנו מוציאים את ה fixtures את ה Sprites ואת ה names מכל contact שקיים במערך contacts_ של MyContactListener

מהרגע שיש לנו את המידע הזה על הקשר, אנחנו יכולים להחליט אייך לטפל בו:

// 20.8 fire touches enemies body
 if((isFireName(nameA) && isEnemyBodyName(nameB)) ||
 (isFireName(nameB) && isEnemyBodyName(nameA))){
var eName2:String;
if(isFireName(nameA)){
eName2 = nameB;
}
else{
eName2 = nameA;
}
var tmpEnemy2:EnemyActor = _enemiesManager.getEnemyByName(eName2);
if(tmpEnemy2 && tmpEnemy2._isObstical == false){
tmpEnemy2.handleGotHit(true);
}
  
 }

אז זו הדוגמה של אייך אנחנו בודקים אם אש של השחקן שלנו פגעה באויב, אנחנו בודקים את זה די בקלות באמצעות השמות שהוצאנו, וכיוון שהשם של האויב הוא ספציפי לכל אויב, אפשר להשתמש בו כדי למצוא את האויב שבמערך של ה enemiesManager (מחלקה שאני משתמש בה לניהול כל האויבים במשחק) ואז אפשר לקרוא לפונקציה handleGotHit שמטפלת בכל השינויים הגרפיים והלוגיים הנחוצים לטיפול באויב שנפגע.

וזה בעצם הכל - סיימנו לעבור על הנקודות החשובות והעיקריות בבנית משחק פיזיקאלי עם box2d. יש מן הסתם עוד המון דברים שאפשר לעשות עם box2d אבל זה הבסיס. אם זה מעניין אתכם, אז זה הזמן להתחיל לחפור ולעשות קצת טוטוריאלים.  רק שימוש לב שהטוטוריאלים שאתם עובדים איתם עדכניים מספיק, כי יש דברים כמו השימוש ב fixtures שנכנסו רק מגירסה 2.1 בפלאש. 

מקווה שנהניתם, בכל מקרה אם יש לכם שאלות נוספות, אתם מוזמנים להשאיר תגובה.






יום ראשון, 11 בדצמבר 2011

לו רק היה לי חזרזיר לשנוא (פיזיקה במשחקים חלק 2)

הפוסט הזה הוא המשך לדיון על משחקים פיזיקאלים שהתחלתי בפוסט הקודם


מה שעשינו עד עכשיו היה לראות אייך מגדירים את העולם הפיזיקאלי, ולהפעיל את הלולאה של המשחק, מבחינת ההפעלה של האוביקטים הפיזיקאלים.


עכשיו נתחיל בלהסתכל אייך מגדירים את האוביקטים הפיזיקאליים עצמם.


קודם כל, מה זה אומר להגדיר אוביקט פיזיקאלי? ובכן הכוונה היא פשוט להגדיר את התכונות הפיזיקליות שלו. למשל לאוביקט שנגדיר יש צורה, שמשפיעה על המשקל שלו, ועל אייך הוא יתקל באוביקטים אחרים. יש לו את החיכוך שלו, שאומר  שכאשר הוא נתקע באוביקטים אחרים, כמה הוא יושפע מהם, וכמה הם ממנו, ואייך. יש גם את הקפיציות שלו ותכונות נוספות.


כל הדברים האלה ישפיעו על המשחקיות שלנו, כי הם יקבע אייך האויביקט יתנהג במשחק. וכל התכונות מסוג זה דומות מאוד לתכונות שיש לאוביקטים במציאות. אבל במשחק יש עוד דברים שמשפיעים על התפקוד של האוביקט, ואחד מהם הוא, מהו סוג האוביקט הפיזיקאלי.


"סוג האוביקט הפיזיקאלי" זה ביטוי קצת כבד לרעיון די פשוט, שנועד לפשט את העבודה עם האוביקטים. יש 3 סוגי אוביקטים פיזיקאלים ב box2d:


Dynamic
Static
Kinematic


הסוג Dynamic הוא פשוט תיאור לאוביקטים שעובדים די דומה לעולם האמיתי, ומושפעים מכוח המשיכה, ומהתנגשויות וכוחות שפועלים עליהם.


הסוג Static הוא פשוט... משהו סטטי. נועד לדברים שלא זזים בכלל, ולא מושפעים מכוח המשיכה, ושאם אוביקט דינמי פוגע בהם, אז בעוד שהוא יושפע מהם, הם לא יושפעו ממנו. בלי להסתבך יותר מידי במה זה אומר, תחשבו על משחק דו מימדי עם פלטפורמה, אפשר לקפוץ עליה, ללכת עליה, לירות עליה, והיא לא תזוז. וזאת למרות שהרבה פעמים היא תהיה תלויה בעביר בלי שום תמיכה נראית לעין. 
זה בדיוק היתרון, בעוד שבחיים האמיתים, אנחנו צריכים למנוע מדברים עם משקל ליפול, בעולם של box2d זה קל ופשוט, מספיק שהגדרתם משהו כסטטי, הוא יעמוד באוויר כאילו כלום.


הסוג האחרון הוא Kinematic. פה החלטתי להעזר במורפיקס, כדי לראות אם הוא יוכל לעזור לי להגדיר מה זה אוביקט קינמטי יותר טוב, ולשמחתי ההגדרה מדברת בעד עצמה: 


Kinematic  - קינמטי, ששייך או קשור לקינמטיקה, לחקר תנועתם של גופים בהתעלם מכוחות הפועלים עליהם.


(אני משער שאם הייתי לומד פיזיקה בתיכון הייתי כבר מכיר את ההגדרה, אבל לא נורא, כל יום לומדים משהו חדש) ובכן, מדובר באוביקטים, שבתחלס מתנהגים בדיוק כמו אוביקטים סטטים, פרט לכך שאפשר להזיז אותם - לתת להם אנרגיה  קינטית. נשאלת השאלה למה זה טוב?
והתשובה, זה טוב להרבה דברים. החל מציפור שעפה באוויר, ואיננה מושפעת מכוח המשיכה (בלי שנצטרך להגדיר כוח של כנפים, חיכוך עם האוויר וכו), יריות שאנחנו רוצים שלא נראה את השפעת כוח המשיכה עליהם, אבל שיהיה אפשר להניע אותם יחסית לאט על המסך, כך שנוכל לראות אותן. מטבעות מרחפים באוויר וכו'


אז עכשיו אחרי שדיברנו על שלושת סוגי האוביקטים הפיזיקאלים, וגם דיברנו קצת על התכונות שלהם, קל להבין איזה גמישות הם נותנים לנו בהגדרה של אוביקטים למשחק. מן הסתם שמתם לב, ש Kinematic, ו Static הם סוג של מעגלי פינות, כדי להפוך את החיים שלנו ליותר קלים. ובכן יש עוד סוג אחד של עיגול פינה, שאני מעדיף להזכיר כבר עכשיו.


ה"עיגול פינה" הזה, הוא הגדרה של אוביקט כ sensor. למעשה זו לא הגדרה שמתחייבת לכל האוביקט, יכולה להיות רק לחלק לו, אבל בואו נגיד למשל שיש לנו דמות שנעה במסך, והיא יכולה לאסוף מטבעות, לבבות, מפתחות וכדומה. אז מצד אחד אנחנו רוצים לזהות התנגשות עם האוביקטים האלה, מצד שני, אנחנו לא רוצים שאם הדמות שלנו רצה או קופצת, ונוגעת באחד האוביקטים האלה היא תעצר. אנחנו רוצים לדעת על זה לטפל בזה, אבל שהדמות תמשיך כאילו כלום, בשביל זה נגדיר אוביקטים ( או חלקים שלהם) כסנסורים.


עכשיו עברנו על רוב הדברים החשובים, הגיע  הזמן לעזוב את הפילוסופיה, ולראות קצת קוד. 

public class Actor extends EventDispatcher {

protected var _body:b2Body
protected var _costume:DisplayObjectContainer;
public function Actor(myBody:b2Body, myCostume:DisplayObjectContainer) {
// constructor code
_body = myBody;
_body.SetUserData(this);
_costume = myCostume;
updateMyLook();
}
public function updateNow():void{
updateMyLook();
childSpecificUpdate();
}

protected function childSpecificUpdate():void{
// function does nothing
// might be called by children
}
public function destroy():void{
//remove actor from world
// remove even listeners - misc cleanup
cleanupBeforeRemoving();
// remove the custom sprite from the display
_costume.parent.removeChild(_costume);
//destroy body
PhysicsVars.world.DestroyBody(_body);
}
protected function cleanupBeforeRemoving():void{
// function does nothing
// might be called by children
}
protected function updateMyLook():void{
_costume.x =  _body.GetPosition().x *PhysicsVars.RATIO;
_costume.y = _body.GetPosition().y *PhysicsVars.RATIO;
_costume.rotation = _body.GetAngle()*(180/Math.PI);
}
public function get body():b2Body { return _body;}
public function get costume():DisplayObject{ return _costume;}
}


קודם כל אנחנו מסתכלים במחלקה Actor שזו המחלקה שכל שאר ה Actors יורשים ממנה. הרעיון הוא שמחלקת הבסיס, תשמור ותטפל בכל מה שרלוונטי לכל מי שיורש ממנה. ובקיצור היא מחזיקה משתנה בשם body שהוא הגוף הפיזיקאלי, costume שמייצג את הצד הויזואלי (שאני אפילו לא סגור אם אייתתי נכון...) והיא דואגת שהם ינועו בהתאמה אחד לשני.

public function Actor(myBody:b2Body, myCostume:DisplayObjectContainer) {
// constructor code
_body = myBody;
_body.SetUserData(this);
_costume = myCostume;
updateMyLook();
}

הפונקציה מעל היא כמובן ה constructor. היא בנויה באופן כזה, שב Actors השונים, שיורשים מהמחלקה Actor אנחנו נגדיר את מה שצריך להגדיר כדי לבנות את ה body ואת ה costume ואז נעביר אותם לפונקציה הזאת, שהיא ה super constructor (ה constructor של האבא). 

ה SetUserData פשוט משתמש במצביע שנמצא ב body כדי להצביע ל Actor שלו, זה שימושי כי זה מאפשר לנו להגיע למשל מ body שזיהינו התנגשות לגביו, ל Actor שעליו נצטרך לעשות פעולות מסוימות. למשל אם יש יריה שיצאה מהמסך, נרצה להרוס אותה, כי אין לנו בה צורך עוד, והיא סתם תכביד על המשחק (במיוחד אם יווצרו עם הזמן עוד המון כמוה) אז אפשר לזהות שה body יצא מחוץ למסך (למשל על ידי זה שהוא פגע באוביקט שמשמש כ danger zone מחוץ למסך, ונועד לתפוס את מה שיצא ממנו) ואפשר דרכו לשחרר גם אותו ואם את ה costume, ולעשות עוד דברים אם נדרשים (למשל לאפשר לשחקן לירות שוב, או לשמור סטיסטיקות) 


בסוף הקונסטרקטור, אנחנו קוראים לפונקציה הבאה:

protected function updateMyLook():void{
_costume.x =  _body.GetPosition().x *PhysicsVars.RATIO;
_costume.y = _body.GetPosition().y *PhysicsVars.RATIO;
_costume.rotation = _body.GetAngle()*(180/Math.PI);
}

הפונציה הזאת דואגת להצמדה במיקום בין ה costume ל body. למעשה העולם הפיזיקאלי סוג של חי בפני עצמו, האוביקטים נעים שם לפי החוקים שלו, ומשפיעים אחד על השני. אבל ה Sprite שלנו (הרי הם ה costumes) הם לא חלק מהעולם הפיזיקאלי. אז אם לא נעשה משהו שיגרום להם לנוע בהתאמה לאוביקטים הפיזיקלים, אז בעצם לא תהיה לנו פיזיקה במשחק.
אז פה אנחנו עושים את זה אנחנו מעדכנים את ה x וה y. להזכירכם ה PhysicsVars.RATIO הוא משתנה שמיצג את היחס בין גודל בנקודות במסך למטר בעולם הפיזיקאלי, ומוכר גם כ PTM RATIO. לכן אנחנו מכפילים את ערכי ה x וה y של ה body שחי בעולם של יחידות של מטרים, ב ration שיתן לנו את הגודל בנקודות שמתאים לעבודה עם spriteים. (ושוב נזכיר, במקרה שלנו נקודות זה כמו פיקסלים -> כל 30 נקודות הם מטר אחד). 

בשורה האחרונה אנחנו מסובבים את ה costume בהתאמה ל sprite גם פה יש לנו איזה חישוב המרה, שבגדול פשוט עושה המרה מרדיאנים למעלות. (לא נכנס לעומק של זה כרגע, אפשר לקחת ולהשתמש בקוד הזה כמו שהוא. ולמידע נוסף לחפש משהו כמו radians to degrees או להיפך, בידידנו הטוב גוגל) 

נקודה חשובה לזכור: לפונקציה הזאת אנחנו נמשיך ונקרא בכל loop של התקדמות במשחק, עבור כל ה Actorsהפעילים. רק על ידי זה, אנחנו יכולים לגרום לכל ה costumes לנוע עם כל ה bodies המתאימים להם, וליצור את האשליה שה costumes הם שעובדים פיזיקאלית.

אוקי, זה כל מה שהיה באמת חשוב במחלקה של Actor. את ה"בשר" האמיתי נמצא במחלקות שיורשות ממנה, ושנועדו להגדיר Actorים ספציפיים. 

הקוד הבא הולך להראות יותר מסובך ממה שראינו עד עכשיו, אבל אל תדאגו, אני מראש מפרק אותו לחתיכות ומסביר כל חלק בנפרד. ויש גם מה להסביר, כי יש פה עוד דברים שלא דיברנו עליהם.  בנוסף לזה, אני מסיר חלקים ממנו שלא רלוונטיים לדיון.

החלק הראשון של ה constructor, החלק בו מגדירים את ה costume.

public function FireActor(aParent:DisplayObjectContainer,fireType:int, pos:Point) {
// constructor code
// 20. type, id and costume
_fireType = fireType;
var tmpCostume:MovieClip = new FiresAnimations();
FiresAnimations(tmpCostume).setupFire(_fireType);
tmpCostume.x = pos.x;
tmpCostume.y = pos.y;
aParent.addChild(tmpCostume);


פה אנחנו יוצרים את ה FireActor מחלקה שיורשת מ Fire ונועדה לטפל ביריות.

_fireType = fireType;

אחנו שומרים את ה fireType, כי אנחנו מטפלים קצת שונה בסוגי היריות השונות.

       var tmpCostume:MovieClip = new FiresAnimations();
      FiresAnimations(tmpCostume).setupFire(_fireType);

ה FiresAnimations זה בעצם מחלקה שמוצמדת ל MovieClip שמכיל את כל האנימציות והתצוגות של סוגי היריות השונים, זה בעצם ה Costume של ה Actor שאנחנו דואגים גם להוסיף לסצינה.

עד כאן החלק של מה שאנחנו רואים, כעת הגיע הזמן להגדיר סוף סוף את האוביקט הפיזיקאלי: 



//30. vars for body & shape
var aBody:b2Body;
var aBodyDef:b2BodyDef;
var aBodyShape:b2CircleShape;
var aBodyFixtureDef:b2FixtureDef;
נתחיל עם המשתנים שנצטרך, יש את ה body שזה הגוף עצמו. יש BodyDef שזה ההגדרות לפיהם נבנה את הגוף, יש את ה Shape במקרה שלנו צורה מעגלית b2CircleShape.  ויש את ה Fixture שזה אפשר אולי להגדיר כ תצורה של האוביקט, שנובעת מהצורה.

הרבה חלקים לאוביקט אחד...

זה בגלל שהחלוקה הזאת עוזרת לנו להגדיר ולטפל בכל החלקים של ה body. וזה שהם מיוצגים בנפרד, עוזר לנו בטיפול. על ה body עצמו אפשר לחשוב בתור מרחב. מעין מרחב שכל האוביקטים בו הם חלק מאותו body. (די דומה ל movie clip שמכיל בתוכו אוביקטים גרפיים אחרים).
אי לכך, ה BodyDef זו ההגדרה הבסיסית של המרחב הזה. הנקודה שלפיה הוא ממוקם, הסוג של ה body שמשפיע על התנהגות כל מה שבתוך ה body עם שאר העולם הפיזיקאלי.

הצורה Shape היא למעשה חלק מה Fixture. היא באה ליצג צורה כלשהי באוביקט. שימו לב - צורה באוביקט, ולא כל האוביקט. למעשה אוביקט יכול להיות מורכב מכמה fixtures כשלכל אחת צורה משלה. בהרבה מקרים זה חייב להיות ככה. בואו נסביר. 

קודם כל חשוב להבין, שחישובים פיזיקלים זה דבר מסובך. ככל שהאוביקט הפיזיקאלי הוא יותר מורכב, כך יהיו יותר חישובים, וכך יש סיכוי יותר גדול שנחווה לאגים במשחק שלנו. ניתן דוגמה פשוטה. 

נניח שיש לנו שני עיגולים, אייך יודעים אם הם מתנגשים? זה די פשוט, אם המרחק בין המרכזים שלהם, קטן מסכום הרדיוסים שלהם, אז הם בהכרח מתנגשים. זו אחת הסיבות שאם אפשר להשתמש בעיגולים תשתמשו בעיגולים.

מה לגבי מלבנים? האמת שאני לא זוכר את האלגוריתם, אבל זה משהו יותר מסובך. אפילו אם הם בזווית ישרה (כלומר מקבילים לריצפה) זה מן חישוב עם ה xים וה yים - בקיצור, יותר מסובך מעיגולים. אם מסובבים אותם, זה כבר בכלל מתחיל להיות מסובך, ואני אפילו לא אתחיל להסביר מה קורה עם פוליגונים (ניסיתי  פעם ללמוד את זה ולישם את זה - לא פשוט בכלל).

אבל זה בדיוק היתרון במנועים כמו box2d, הם עושים את זה בשבילנו. אבל זה שאנחנו לא צריכים לחשב את הכל, לא אומר שאנחנו לא צריכים להבין ולהתחשב במגבלות של box2d. 

נקצר ונגיד רק שב box2d גם עם עובדים עם פוליגונים, עובדים עם פוליגונים עד 8 קודקודים, דואגים ליצור את הקודקודים בכיוון מסוים (נגד או עם כיוון השעון, אני לא זוכר את הכיוון, כי זה בתחלס שונה מ פלאש ל objective c) כמו כן חשוב לדאוג שהם יהיו בצורה שלא יהיו להם זוויות פנימה.

אוקי... לפני שאני גורם לכם לשבור את המקלדת ביאוש, אני רק אגיד שיש כלים מצוינים, כמו Physics Editor, שעוזרים לכם לבנות את כל הצורות שתרצו בצורה קלה ונוחה והם, כבר מחלקים את המבנה ל Fixtures ולכם רק חשוב לזכור, לא לשים מלא קודקודים לכל אוביקט, אפשר לעשות המון במספר די קטן של קודקודים. 

ואוו - זה כבר נהיה די ארוך, בואו נתקדם.




//40. set bodyDef, position && type
aBodyDef = new b2BodyDef();
aBodyDef.position.Set(pos.x/PhysicsVars.RATIO, (pos.y)/PhysicsVars.RATIO);

אז למעלה אנחנו רואים הגדרה של bodyDef עם המיקום (שימו לב שהפעם חילקנו ב Ratio) 

                      if(_fireType == 1){
aBodyDef.type = b2Body.b2_kinematicBody ;
aBodyShape = new b2CircleShape(24/PhysicsVars.RATIO);
}
else if(_fireType == 2){
aBodyDef.type = b2Body.b2_dynamicBody;
aBodyShape = new b2CircleShape(12/PhysicsVars.RATIO);
}



פה למעלה יש לנו דוגמה טובה לאיך אפשר להחליט על פי סוג היריה, ליצר צורה ו bodyDef שונים ליריות השונות. שמתם לב בוודאי לקוטר השונה, ולזה שיריה אחת היא קנמטית בעוד שהשניה היא דינמית. תחשבו על זה כאילו היריה הראשונה היא יריה מאקדח, שנעה בקו ישר, והשניה היא כמו רימון קופץ, שמושפע מכוח המשיכה.

בשני המקרים יש לנו צורה של עיגול. אבל אפשר בקלות יחסית ליצור גם מלבן לדוגמה:

                       var rBodyShape:b2PolygonShape = new b2PolygonShape();  
if(_fireType == 3){ // lightning
rBodyShape.SetAsBox(340/PhysicsVars.RATIO, 60/PhysicsVars.RATIO);
aBodyFixtureDef.shape = rBodyShape;
}

האמת, שחשבתי בהתחלה לוותר על להראות קוד שקשור ליריה מהסוג הזה, אבל זו דוגמה טובה ליצירה של קופסה. הגדלים הם חצי הרוחב וחצי הגובה, כך שהקופסה הזאת באמת היא בגודל 680 על 120.
והשורה הזאת:
aBodyFixtureDef.shape = rBodyShape;

היא כבר ממש ההגדרה של הצורה כחלק מהתצורה, ה fixture


בכל מקרה, אחרי שיש לנו צורה, אפשר ליצור fixture בואו נסתכל על כל התהליך של יצירת fixture:

// 60. fixture

aBodyFixtureDef = new b2FixtureDef();
        aBodyFixtureDef.shape = aBodyShape;
aBodyFixtureDef.shape = aBodyShape;
aBodyFixtureDef.density = 10.0;
aBodyFixtureDef.filter.categoryBits = 1;
aBodyFixtureDef.filter.maskBits = 65535 - 4;
aBodyFixtureDef.friction =0;
aBodyFixtureDef.restitution=1;
//70 user data
tmpCostume.name = "fireType_"+_fireType+"_ID_"+_id;
aBodyFixtureDef.userData = tmpCostume;


יש פה הרבה דברים, וזה אחרי ששוב הורדתי דברים לא רלוונטים לדיון. בגדול, מגדירים את ה fixtureDef.  מגדירים באיזו צורה משתמשים, ואחרי זה דברים כמו חיכוך, קפיציות. פה גם אפשר להגדיר, אם ה fixture היא  צורה מסוג sensor, כלומר, האם היא תגיב להתנגשויות מבחינת פיזיקה או רק תזהה אותם. 

יש לנו פה עוד שני דברים חשובים, האחד הוא ה categoryBits , וה maskBits שהם חלק מהפילטר. 
השני זה ה userData של ה fixture. 

ה box2d נותן לנו כלי מצוין להחליט מה אנחנו רוצים שיתנגש במה. ה category bits מגדירים את הקטגוריה של ה fixture, ומשייכים אותו לקבוצה. וה mask bits, מיצגים את כל הקבוצות שה fixture הזה יכול להתנגש איתם. 
לא מדובר רק בהתנגשות פיזיקאלית, למעשה אם נגיד שיש אויב שה category bits שלו הוא 16, וה mask bits של יריה מסוג מסויים הוא 65535-16 אז כל לא ישמר, ולא תהיה שום התיחסות בין כל מגע של היריה עם האויב.  המספרים עצמם, הם חזקות של 2, (למבינים בינארית, יצוג של 16 ביטים, כלומר 16 קבוצות אפשריות) 
אם זה מבלבל אתכם קצת תלכו לפי הכלל הזה. לכל קבוצה תנו ב category bits קוד של חזקה של 2 (1,2,4,8,16,32 וכן הלאה) וב mask bits תנו 65535 פחות כל הקודים שאתם לא רוצים שהקבוצה הזאת תגע בהם.

ה user data של ה fixture עוזר לנו לדעת באיזה חלק של ה body פגענו. הרי body יכול גם להיות מורכב מכמה fixture, למשל אחד בצורת מלבן לראש, ואחד בצורת עיגול לגוף. זה אולי לא נשמע הגיוני ככה, אבל תחשבו על אויב במריו - אפשר לקפוץ לו על הראש ולהרוג אותו, אבל אם פוגעים לו בגוף אז נפגעים. אז אם נבנה גוף עם מלבן לראש, שהוא sensor ועיגול לגוף, ונדע אייך להבחין בינהם, אז נוכל לדעת מתי הדמות של השחקן היא זו שנפגעת ומתי זו של האויב.

בעצם מה שעשיתי זה הכנסתי את ה costume (או כל MovieClip או Sprite יכולים לעבוד) ל userData ודאגתי שהשם שלו יהיה משהו שאני אוכל לזהות בקלות. אני מקווה שבהמשך תהיה לנו הזדמנות לראות, אייך המידע הזה הופך שימושי בהתנגשויות.

עכשיו אחרי שהגדרנו את ה fixture Def נותר לנו ליצור את ה body ואז ליצור לו את ה fixture:

// 80. handling the body
aBody = PhysicsVars.world.CreateBody(aBodyDef);
aBody.CreateFixture(aBodyFixtureDef);
aBody.GetMass();


ולסיום, מה שנותר זה כמו שאמרנו, לשלוח את ה body וה costume ל super constructor

// 150. updating in actor
super(aBody, tmpCostume);

ולמעשה זה כל החלקים החשובים לנו מבחינת הדיון בנושא הפיזיקאלי, בנוגע ל Actors. אולי תתפלאו לשמוע אבל כבר כיסינו כמעט הכל. אם איני טועה מה שנותר לנו זה להסתכל קצת ב contact listener כדי להבין קצת יותר מה קורה שם מבחינת זיהוי ההתנגשויות, ולהסתכל בפונקציה checkAndHandleCollision שקוראים לה כחלק מה loop של המחשק על מנת לטפל בהתנגשויות שזוהו. 

אני יודע שאני עובר פה על דברים קצת ב high level, אז אם יש שאלות יותר ספציפיות, אתם מוזמנים לשאול בתגובות.

ונתראה בקרוב עם החלק שלישי (והאחרון?) של הדיון על פיזיקה עם box2d במשחקים.


ו... כמעט שכחתי (תודה מיחדת לחבר'ה מ FXP שהזכירו לי) רציתי לתת דוגמה למשחק פיזיקאלי בפלאש. למעשה זה המשחק שהוא המקור לקוד הזה. זה משחק שנכתב ל iPhone על ידי בן קימל, ועוצב על ידי מייק (שעשה את הקונספט ארט) ונריה. שעבדו איתי ב humanGNA. אני המרתי אותו לפלאש (למעשה כמעט כתבתי מחדש, בגלל הבדלי שפות פיתוח). 


וזה המשחק, Mishu the Dragon