אנדישלום חברים יקרים!

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

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

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

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

אז בואו נחזור לנושא השיעור שלנו, אנימציה בסיסית.

קדימה לעבודה

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

אנדי, נראה שאתה מדבר על "ריבוי תהליכים"! זה נושא כל כך מרתק! אני יכול לדבר שעות על תקשורת בין תהליכים, Deadlock-ים, Critical Sections, שלא לדבר על Semaphor-ים ו- Mutex-ים.

אנדיפרופסור, אתה תבריח לנו את כל הקוראים!
בוא נסתפק בהסבר בסיסי על Process-ים ו- Thread-ים, ונמליץ לקוראינו הנאמנים לחפש ברחבי הרשת עוד על נושאים מתקדמים אלו.

רוב מערכות ההפעלה המודרניות, ובכלל זה אנדרואיד, מאפשרת לנו להריץ קטעי קוד במקביל באמצעות Process-ים ו- Thread-ים.

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

Thread - זהו קטע קוד הבסיסי ביותר שמערכת ההפעלה מסוגלת להריץ כיחידת קוד עצמאית. כל Process יכול להכיל Thread אחד או יותר.

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

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

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

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

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

טוב, נחזור לשיעור.
בואו ונפתח את תַּהֲלִיכוֹן הראשון שלנו.

שלב א – הרחבת מחלקת הכדור

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

כיוון תזוזת הכדור על ציר המסך

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

להלן הקוד החדש (והמורחב) של מחלקת הכדור

package iAndroid.pingPong;

public class Ball {
	private int x;
	private int y;

	private int maxWidth;
	private int maxHeight;

	private boolean movingRight;
	private boolean movingBottom;

	// Receive the start position of the ball and screen dimensions
	public Ball (int startX, int startY, int screenWidth, int screenHight)
	{
		this.x = startX;
		this.y = startY;

		this.maxWidth = screenWidth;
		this.maxHeight = screenHight;

		this.movingBottom = false;
		this.movingRight = false;
	}

	// Return the ball horizontal position
	public int getX()
	{
		return this.x;
	}

	// Return the ball vertical position
	public int getY()
	{
		return this.y;
	}

	// Moving the ball one step.
	public void moveBall()
	{
		// Moving the ball
		if (movingRight)
			this.x++;
		else
			this.x--;

		if (movingBottom)
			this.y++;
		else
			this.y--;

		// Check if we need to change the ball direction horizontally
		if (this.x >= this.maxWidth)
			this.movingRight = false;

		else if(this.x <= 1)
			this.movingRight = true;

		// Check if we need to change the ball direction vertically
		if (this.y >= this.maxHeight)
			this.movingBottom = false;
		else if (this.y <= 1)
			this.movingBottom = true;
	}
}

בואו נעבור על הקוד החדש שלנו:
שורות 7-8: הגדרה של משתנים פרטיים maxWidth ו maxHeight אשר יחזיקו את הרוחב והגובה המקסימליים בהם הכדור יכול לזוז.
שורה 10: הגדרה של משתנה פרטי בשם movingRight מסוג boolean, אשר אם יהיה true הוא יציין שהכדור זז ימינה ואם false, אז שהכדור זז שמאלה.
שורה 10: הגדרה של משתנה פרטי בשם movingRight מסוג boolean, אשר אם יהיה true הוא יציין שהכדור זז ימינה ואם false, אז שהכדור זז שמאלה .
שורה 11: הגדרה של משתנה פרטי בשם movingBottom מסוג boolean, אשר אם יהיה true הוא יציין שהכדור זז למטה ואם false, אז שהכדור זז למעלה.
שורה 14: הגדרת הבנאי של מחלקת הכדור, אשר מקבל 2 פרמטרים נוספים והם screenWidth המגדיר את אורך המסך, ואת screenHeight המגדיר את גובה המסך.
שורות 19-20: הכנסת אורך וגובה המסך, שהתקבלו כפרמטרים לבנאי, לתוך maxWidth ו maxHeight.
שורות 22-23: הגדרת הכיוון הראשוני אליו יזוז הכדור, והוא  למעלה (movingBottom = false) שמאלה (movingRight = false).
שורה 39: הגדרה של מתודה חדשה בשם moveBall אשר אחראית על הזזת הכדור.
שורה 42-43: אנו בודקים אם הכדור זז ימינה, ואם כן, אנו מוסיפים למיקום האופקי של הכדור 1.
שורות  44-45: אחרת, אנו מחסירים  מהמיקום האופקי 1.
שורות 47-48: אנו בודקים אם הכדור זז למטה, ואם כן, אנו מוסיפים למיקום האנכי של הכדור 1.
שורה 49-50: אחרת, אנו מחסירים  מהמיקום האנכי 1.
שורות 53-54: אנו בודקים האם הכדור נמצא בצידו הימני ביותר (המקסימלי) של המרחב, ואם כן אנו משנים את כיוונו.
שורות 56-57: אחרת, אנו בודקים האם הכדור נמצא בצידו השמאלי ביותר של המרחב, ואם כן אנו משנים את כיוונו.
שורות 60-61: אנו בודקים האם הכדור נמצא בחלקו התחתון ביותר (המקסימלי) של המרחב, ואם כן אנו משנים את כיוונו.
שורות 62-63: אנו בודקים האם הכדור נמצא בחלקו העליון ביותר של המרחב, ואם כן אנו משנים את כיוונו.

שלב ב – יצירת תהליך שמזיז את הכדור

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

אז מה צריך כדי ליצור Thread חדש?
פשוט מאד! כל מה שצריך זה ליצור מחלקה חדשה ולרשת את מחלקת Thread של ג'אווה.
צרו מחלקה חדשה (אתם כבר צריכים לדעת איך) בשם PingPongGame היורשת ממחלקת Thread

יצירת מחלקה חדשה בשם PingPongGame למשחק הפינג פונג באנדרואיד

למחלקת האב, java.lang.Thread ישנה מתודה בשם run שאם אנחנו נממש אותה (כלומר, נרחיב אותה), נוכל לקרוא לה כדי "להפעיל" את ה Thread שלנו.
בנוסף, עלינו ליצור בנאי שמקבל את הכדור ואת ה View המרכזי של המשחק.
להלן קוד המחלקה PingPongGame:

package iAndroid.pingPong;

public class PingPongGame extends Thread {
	private Ball gameBall;
	private PingPongView gameView;

	public PingPongGame(Ball theBall, PingPongView mainView)
	{
		this.gameBall = theBall;
		this.gameView = mainView;
	}

	@Override
	public void run()
	{
		while (1 < 2){
			this.gameBall.moveBall();
			this.gameView.postInvalidate();

			try
			{
				PingPongGame.sleep(5);

			}
			catch (InterruptedException e)
			{
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

		}
	}

}

כמו תמיד, בואו נעבור על קוד המחלקה:
שורות 4-5: הגדרת המשתנים הפרטיים gameBall ו gameView שיחזיקו את הכדור ואת ה View המרכזי.
שורות 7-11: הגדרת בנאי המחלקה שמיישב את המשתנים gameBall ו gameView.
שורה 14: מימוש של המתודה run אשר מוגדרת במקור בתוך מחלקת האב, Thread.
שורה 16: הגדרה של לולאת המשחק המרכזית, שאמורה (בשלב זה) לפעול כל עוד 1 קטן מ 2 (כלומר, לולאה אין-סופית).
שורה 17: קריאה ל moveBall כדי להזיז את הכדור.
שורה 18: קריאה למתודה postInvalidation בתוך ה View המרכזי, כדי להודיע שמשהו השתנה וצריך לצייר את המסך מחדש (בעזרת onDraw כמובן).
שורה 22: קריאה למתודה הסטטית sleep, כדי לגרום לה Thread שלנו "ללכת לישון" למשך 5 אלפיות השניה.

כמה הסברים נדרשים לגבי קטע הקוד הזה:

postInvalidation

בכך שיצרנו Thread נוסף שרץ במקביל לתוכנית המקורית, יצרנו בעצם מצב בו פיצלנו את התוכנה שלנו למספר חלקים (או מספר תהליכונים).
התהליכון PingPongGame, שאחראי לעדכן את מצב המשחק, רץ באופן עצמאי ומנותק מהתהליכון הראשי, אשר אחראי לציור המסך בפועל.
הקריאה למתודה postInvalidation היא דרך לתקשר בין התהליכונים ולהעביר מסר לתהליכון הראשי שה- View שלו דורש עדכון.

sleep

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

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

מחברים את כל החלקים, ומריצים!

כעת, נותר רק לחבר את השימוש במחלקת Ball המשודרגת שלנו ומחלקת PingPongGame החדשה לתוך התוכנית הראשית שלנו.
היכנסו למחלקה PingPong.java ועדכנו את הקוד שיראה כך:

package iAndroid.pingPong;

import android.app.Activity;
import android.os.Bundle;

public class PingPong extends Activity {
	private PingPongView gameView;
	private Ball gameBall;
	private PingPongGame gameThread;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Getting the screen width & height
        int screenWidth = this.getWindowManager().getDefaultDisplay().getWidth();
        int screenHeight = this.getWindowManager().getDefaultDisplay().getHeight();

        // Creating the ball
		gameBall = new Ball(screenWidth / 2, screenHeight / 2, screenWidth, screenHeight);

		// Creating the ball view, and giving it the gameBall as a parameter
        BallView ballView = new BallView(this, gameBall);

        // Creating the game view
        this.gameView = new PingPongView(this);

        // Give the gameView our ballView.
        gameView.setBallView(ballView);

        // Setting the gameView as the main view for the PingPong activity.
        setContentView(gameView);

        gameThread = new PingPongGame(gameBall, gameView);

        // Starting the thread !
        gameThread.start();

    }

}

ושוב, הסבר:
שורות 7-9: יצירת משתנים פרטיים אשר יחזיקו לנו את ה- View של המסך, את הכדור ואת ה Thread של המשחק.
שורות 17-18: שמירת גודל המסך של המכשיר או האמולטור בו אנו מריצים את התוכנה בתוך המשתנים (ותודה למשתמש ididid שסיפק הסבר על כך בפורום)
שורה 21: יצירת עצם חדש מסוג Ball, אשר נקודת ההתחלה שלו היא אמצע המסך ובנוסף אנו מעבירים לו את גבולות המסך עצמו.
שורות 23-33: יצירת ה- View-ים השונים של המשחק, באופן זהה למה שעשינו בשיעור שעבר.
שורה 35: יצירת אובייקט gameThread מסוג PingPongGame שיפעל כתהליכון שירוץ ברקע.
שורה 38: הפעלה של PingPongGame בתהליכון, שרץ ברקע.

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

ועכשיו תלמידים יקרים, מספר אתגרים, עד לשיעור הבא: 1. הוסיפו לקוד את המחלקה של המחבט/ים ודאגו לכך שהמחשב "ישחק" פינג פונג עם עצמו. כלומר, שהמחבט/ים יכה/ו בכדור בכל פעם שהוא מגיע לצד שלו. 2. הוסיפו "אינטיליגנציה מלאכותית" אשר גורמת למחבטים לזוז בצורה כזאת, שהכדור יפגע בכל פעם בחלק אחר של המחבט לדוגמא, פעם הוא יפגע ממש באמצע המחבט, פעם בפינה הימנית ופעם בפינה השמאלית. 3. לבסוף, הוסיפו לוגיקה שמשנה את זווית התזוזה של הכדור, בהתאם לאיפה שהוא פגע במחבט. לדוגמא אם הכדור פגע בפינה הימנית או השמאלית, אז הכדור יזוז בזווית חדה יותר לצדדים, בעוד שאם הוא פגע באמצע, הזווית תהיה חדה יותר לאמצע. נסו ליצור מגוון זוויות שונות ולא רק שלוש. כל אתגר הוא ברמת קושי שונה, כאשר הראשון הוא אתגר בסיסי, השני בינוני/קשה והשלישי קשה. טיפ! לשם ביצוע אתגר מספר 2, נסו להישתמש במחלקה Random, היוצרת מספרים אקראיים.

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

בין הפותרים נכונה נגריל תמונה אישית של הפרופסור בבקיני!

עד לשיעור הבא חברים, להית'!