יום ראשון, 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 בפלאש. 

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






2 תגובות:

  1. יהיה נחמד מאוד אם תוכל לשים בסוף כל מדריך FLA עם כל מה שעשית, ועדיף גם SWF שיהיה אפשר לראות את התוצאות.

    השבמחק
  2. הי איתמר,

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

    השבמחק