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



אין תגובות:

הוסף רשומת תגובה