ping pong activity

Download Report

Transcript ping pong activity

‫משחק הפינג פונג‬
‫הסבר מקוצר לפרויקט הדוגמא‬
‫ע"י עירן דנן‬
‫פעילות היא בסיס ממשקי המשתמש הויזואליים שלנו‪.‬‬
‫כל חלק באפליקציה שנראה על המסך‪ ,‬יהיה בתוך פעילות‬
‫(‪.)ACTIVITY‬‬
‫למעשה‪ ,‬כמו שנראה‪ ,‬רוב ממשקי המשתמש שלנו יהיו בעצם‬
‫"בנים" של פעילות‪ ,‬כלומר מחלקות היורשות את אופי‬
‫הפעילות הכללי ומוסיפות לה‪.‬‬
‫ברירת המחדל כאשר יוצרים פרוייקט חדש היא בד"כ‬
‫פעילות שכותבת "שלום" על המסך ‪.‬‬
‫בואו ניצור את הפרויקט החדש שלנו‪:‬‬
‫באקליפס נבחר‬
‫‪File -> New -> Project‬‬
‫ונקבל את המסך הבא‪:‬‬
‫נבחר באפשרות‬
‫‪Android project‬‬
‫כמו שמסומן‬
‫נגיע למסך הבא‪,‬‬
‫שם נבחר את ה‬
‫‪Android 1.6‬‬
‫כגרסה המינימלית‬
‫של מערכת ההפעלה‬
‫עליה התוכנית שלנו‬
‫תעבוד‪.‬‬
‫נשים לב למלא גם את‬
‫השם בתא העליון‪ ,‬את שם‬
‫החבילה ושם האפליקציה‪.‬‬
‫אם עשינו‬
‫הכל נכון‪ ,‬יפתח‬
‫לנו פרויקט‬
‫בצד השמאלי‬
‫של המסך תחת‬
‫הכותרת‬
‫‪Package explorer‬‬
‫נפתח את הפרויקט‪ ,‬נכנס לתיקייה העליונה‬
‫ונפתח את החבילה היחידה שלנו‪ .‬הקובץ הראשון שם‬
‫הוא הפעילות שלנו‪.‬‬
‫הסבר קטן על‬
‫שורות הקוד‬
‫המופיעות כאן‪:‬‬
‫)‪ - Public void onCreate(Bundle b‬פונקצית ה"לידה" של הפעילות‪ ,‬כאן מגדירים איך‬
‫‪.1‬‬
‫ניצור את הפעילות שלנו ‪.‬‬
‫‪ - Super.onCreate‬לפעילות שלנו יש אב‪ ,‬שבדמותו היא נוצרה‪ ,‬ואליו ניגש בעזרת המילה‬
‫‪.2‬‬
‫‪.super‬‬
‫הבן נוצר בדמותו של האב ולכן יורש את כל הפונקציונילות שלו‪ .‬במקרה הזה הקריאה לבנאי*‬
‫האב תבצע את הקישור למסך‪ ,‬רישום במערכת וכו'‪.‬‬
‫)‪ - setContentView(R.layout.main‬קביעת התוכן הויזואלי של הפעילות ע"י ‪LAYOUT‬‬
‫‪ ,‬שהוא כמו שרטוט חסר פונקציונליות של המסך‪ ,‬שרטוט "יבש"‪.‬‬
‫הפעילות שלנו מוכנה‪ ,‬החלון הגדול מוכן‪ .‬אבל מה נציג בתוכו?‬
‫אפשרות אחת היא פריסת התוכן בלייאאוט‪ ,‬כמו ברירת‬
‫המחדל‪.‬‬
‫במקרה שלנו דרושה קצת גמישות גרפית‪ ,‬אז ניצור‬
‫בעצמנו (דרך הקוד) את התוכן שלנו‪ ,‬ה‪ VIEW‬שלנו‪.‬‬
‫ה ‪ VIEW‬היא בעצם מחלקת האב לכל הרכיבים הגרפיים של‬
‫האפליקציות שלנו‪.‬‬
‫דוגמאות להרחבה של ‪ VIEW‬הן תיבות טקסט‪ ,‬כפתורים‪,‬‬
‫חלונות להצגת תמונות ועוד‪.‬‬
‫למה צריך אב?‬
‫במחלקת ‪ VIEW‬מוגדרות כל השיטות – הפונקציות – שנצטרך בכל‬
‫תוכן גרפי שהוא‪ ,‬כמו מיקום על הציר‪ ,‬גודל החלון והפונקציות‬
‫המנהלות אותם‪.‬‬
‫נניח שאני רוצה ליצור משחק פינג פונג‪ ,‬וגם משחק כדורגל‪.‬‬
‫לשניהם אני צריך להגדיר היכן אני רוצה למקמם במסך‪ ,‬מה צבע‬
‫הרקע‪ ,‬פונקציות המרשות לי לצייר עליהן וכו'‪ .‬כלומר יהיה לי קל‬
‫אם אוכל לרשת את כל התכונות האלה "במכה"‪ ,‬ולהתרכז רק‬
‫באיפיון היחודי של התוכן שלי‪.‬‬
‫לצורך כך נוצרה הורשה בכלל‪ ,‬וב‪ VIEW‬בפרט‪.‬‬
‫עוד קצת על המחלקה ‪:VIEW‬‬
‫‪http://developer.android.com/reference/android/vi‬‬
‫‪ew/View.html‬‬
‫ככלל‪ ,‬נרצה לחלק כל‬
‫תוכנית מחשב שאנו בונים‬
‫לכמה שיותר חלקים‬
‫עצמאיים‪ .‬בפרט בגרפיקה‪,‬‬
‫ובפרט באנימציות שבהן המשתמש משפיע‪.‬‬
‫למשל‪ ,‬במקרה שלנו‪ ,‬יהיה נכון ליצור ‪ VIEW‬של המחבט‪ ,‬ו‪VIEW‬‬
‫של הכדור‪ ,‬ואת שניהם ב‪ VIEW‬ראשי שבו ינועו שניהם‪.‬‬
‫כך‪ ,‬אם נחליט בחודש הבא להוציא את ‪ PingPong2‬לאור שבו‬
‫הכדור יהיה כחול‪ ,‬או המחבט יהיה של נייק‪ ,‬כל מה שנצטרך לעשות‬
‫זה לשנות את ה‪ VIEW‬של הכדור‪ ,‬ולא לצלול לתוך התוכנית‪.‬‬
‫מה שגם‪ ,‬תמיד קל יותר לפתור אוסף של בעיות קטנות מאשר בעיה‬
‫אחת גדולה‪..‬‬
‫מסך המכשירים הניידים שלנו‪ ,‬בדומה למסך המחשב‪ ,‬מורכב‬
‫מפיקסלים‪ ,‬כאשר כל פיקסל יכול להצבע בצבע אחר‪.‬‬
‫מיקום הפיקסלים במסך מיוצג ע"י ‪ X‬ו ‪ ,Y‬כאשר הנקודה‬
‫השמאלית העליונה היא (‪.)0,0‬‬
‫תמונה מסוימת היא בעצם הגדרה מדוייקת של כל הפיקסלים‬
‫במסך‪ .‬למשל פס לבן באמצע מסך ברוחב ‪ 400‬פיקסלים יצוייר‬
‫ע"י צביעת כל הפיקסלים שערך ה ‪ X‬שלהם הוא ‪ 100 ,99‬או ‪101‬‬
‫‪ ,‬בלבן‪ ,‬ואת כל השאר בשחור‪.‬‬
‫איך זה רלוונטי לנו?‬
‫נראה רגע איך מתרחש משחק הפינג פונג‪.‬‬
‫הכדור נע על המסך בין מחבט למחבט‪ ,‬וכאשר הכדור נוגע‬
‫במחבט הוא הופך כיוון ומשנה זווית‪ ,‬בהתאם לזווית הפגיעה‪.‬‬
‫מה זה אומר שהכדור נע בין כתלי המסך?‬
‫או בכלל מנקודה ‪ A‬ל‪?B‬‬
‫זה אומר שהכדור היה בכל נקודה על הקו הישר בין ‪ A‬ל ‪,B‬‬
‫ומכאן שאנו צריכים לצייר אותו בכל נקודה על הקו בין ‪ A‬ל ‪,B‬‬
‫ולמחוק ולצייר בנקודה הבאה וכך הלאה עד שנגיע ל‪.B‬‬
‫תחילה‪ ,‬ניצור את ה ‪ VIEW‬הראשי שלנו‪ .PingPongView ,‬ניצור‬
‫מחלקה חדשה שתירש מ‪ ,VIEW‬כך‪:‬‬
‫נלחץ על המקש הימני בעכבר כשאנחנו על‬
‫החבילה שלנו‪( .‬תזכורת‪ ,‬החבילה היחידה‬
‫שלנו נמצאת בתיקייה ‪.)SRC‬‬
‫בתפריט שיפתח נלך ל ‪New -> Class‬‬
‫ונקבל את המסך הזה ‪>-‬‬
‫בתיבת השם נקליד ‪,PingPongView‬‬
‫בתיבה ‪ superclass‬נמצא את‬
‫‪ Android.view.view‬שזוהי ההגדרה של‬
‫המחלקה ‪ ,VIEW‬ממנה אנו יורשים‪.‬‬
‫נלחץ ‪ ,FINISH‬ונראה שנוספה לנו מחלקה‬
‫בתיקייה‪.‬‬
‫כעת נקבל את המסך‬
‫הבא‪ ,‬ובו שגיאה‪.‬‬
‫כדי לתקן את השגיאה‬
‫נלחץ (כמו בפעמים‬
‫רבות אחרות) על ה‪X‬‬
‫המסומן באדום‪,‬‬
‫ונבחר את האפשרות הראשונה‪ ,‬שתוסיף לנו בנאי שמקבל פרמטר‬
‫אחד בלבד‪.‬‬
‫הסיבה לשגיאה היא שלמחלקת האב ‪ VIEW‬יש רק בנאי שמקבל‬
‫פרמטר‪ ,‬כלומר כדי ליצור מופע של המחלקה שלנו צריך גם כן בנאי‬
‫שמקבל פרמטר‪ .‬עוד על הורשה ובנאים אפשר למצוא בשקפים באתר‬
‫הקורס בנושא הורשה‪.‬‬
‫כעת יצרנו את ה‪ VIEW‬הראשי שלנו‪ ,‬ובו בנאי‪.‬‬
‫איך עובד עניין הציור על המסך?‬
‫המחלקה ‪ VIEW‬מציירת את התוכן שלה בעזרת השיטה‬
‫‪ onDraw‬באופן אוטומטי כאשר התוכנית שלנו עולה‪.‬‬
‫‪ onDraw‬מקבל ‪ ,Canvas‬שהוא כשמו כמו בלוק ציור‪ .‬בכל‬
‫מסך גרפי ישנו ‪ Canvas‬כלשהו‪ .‬ה‪ Canvas‬יודע לצייר בתוכו‬
‫צורות שונות בעזרת שיטות כמו ‪ drawCircle‬למשל‪ ,‬המציירת‬
‫מעגל‪ ,‬ועוד רבות‪ .‬נוסיף את השיטה ‪ onDraw‬למחלקה שלנו‪,‬‬
‫ריקה בינתיים ונקבל את המחלקה הבאה‪:‬‬
import android.content.Context;
import android.graphics.Canvas;
import android.view.View;
public class PingPongView extends View
{
public PingPongView(Context context) {
super(context);
}
@Override
public void onDraw(Canvas canvas) {
‫כאן נצייר את המשחק‬//
}}
}
‫את הכדור נחלק לשתי מחלקות‪:‬‬
‫‪ - Ball .1‬מחלקה שתהווה את החלק הלוגי של הכדור‪ ,‬התכונות וההתנהגות‬
‫שלו‪.‬‬
‫‪ – BallView .2‬מחלקה היורשת מ‪ VIEW‬בדומה ל‪,PingPongView‬‬
‫שיודעת לצייר את הכדור שלנו‪.‬‬
‫האם אפשרי לממש את שני החלקים באותה מחלקה? כמובן‪ .‬בעקרון אפשר‬
‫לעשות כמעט כל תוכנית במחלקה גדולה אחת‪ .‬אז למה לא?‬
‫הפרדה בין חלקים שונים של התוכנית חשובה מכמה סיבות‪ ,‬החשובה מבניהן‬
‫היא שאם נרצה לחזור ולשנות משהו בתוכנית‪ ,‬יהיה לנו הרבה יותר ברור איפה‬
‫השינוי צריך להתבצע כאשר יש הפרדה ברורה בין כל חלק של התוכנית‪.‬‬
‫הפרדה בין חלקי התוכנית מביאה לאי תלות בין חלקי התוכנית‪ ,‬וזה מביא‬
‫לחסכון בזמן ובמאמץ‪.‬‬
‫נחשוב על רכב שמיוצר כולו כחלק אחד "קומפלט"‪ .‬אם חלק אחד יתקלקל‪ ,‬או‬
‫שנרצה לשדרג חלק כלשהו‪ ,‬נצטרך להחליף את כל הרכב‪..‬‬
‫יצירת המחלקה ‪ Ball‬תתבצע בדיוק כמו יצירת המחלקה ‪ ,PingPongView‬רק שבשדה ‪ superclass‬בתפריט לא‬
‫נשנה את ברירת המחדל‪.‬‬
‫המחלקה ‪ Ball‬תראה כך‪:‬‬
‫{‪Public class Ball‬‬
‫‪private int x,y; //‬משתני המיקום של הכדור‬
‫{)‪public Ball (int x,int y‬‬
‫‪this.x = x; //‬הבנאי מציב את המיקום ההתחלתי של הכדור באובייקט החדש שנוצר‬
‫;‪this.y = y‬‬
‫}‬
‫{)(‪public int getX‬‬
‫‪return x; //‬שיטות גישה למיקום הכדור‪ ,‬כדי שנוכל לגשת למיקומו מבחוץ‬
‫}‬
‫{)(‪public int getY‬‬
‫;‪return y‬‬
‫}‬
:‫ כך‬,‫ למשל‬.‫ שהוא יהיה הכדור שלנו‬,‫ נצייר עיגול‬onDraw ‫ ובשיטה‬,‫ שוב‬VIEW‫ נירש מ‬,‫ כאמור‬.‫ שהיא תהיה אחראית על ציור הכדור‬,BallView ‫כעת ניצור את המחלקה‬
import android.graphics.Canvas;
import android.graphics.Color;
import android.content.Context;
import android.graphics.Paint;
import android.view.View;
public class BallView extends View {
private Ball theBall;
private Paint ballPaint;
public BallView(Context context, Ball newBall) {
super(context);
this.theBall = newBall;
{
private Paint getBallPaint(){
‫ למעשה את המברשת המציירת את הכדור‬,‫ניצור צבע חדש‬if (ballPaint == null)
{
ballPaint = new Paint();
ballPaint.setStrokeWidth(2);
ballPaint.setColor(Color.WHITE);
}
return ballPaint;
}
@Override
public void onDraw(Canvas canvas) {
// Drawing the ball
canvas.drawCircle(theBall.getX(), theBall.getY(), 15, getBallPaint());
}
}
//
‫נחזור כעת ל‪.PingPongView‬‬
‫אנו צריכים לשנות את השיטה ‪ onDraw‬שלו כדי שתייצר אובייקט כדור‪ .‬נוסיף מתשנה‬
‫מסוג ‪ BallView‬כך‪:‬‬
‫;‪Private BallView ballView‬‬
‫ונוסיף לו שיטת גישה כדי שנוכל לאתחל אותו‪ ,‬כך‪:‬‬
‫{)‪Public void setBallView(BallView b‬‬
‫;‪ballView = b‬‬
‫}‬
‫כעת‪ ,‬כשיש לנו כדור ב‪ ,PingPongView‬אפשר לשנות את ‪ onDraw‬כך שתצייר אותו‪,‬‬
‫כך‪:‬‬
‫{)‪Public void onDraw(Canvas canvas‬‬
‫;)‪ballView.onDraw(canvas‬‬
‫}‬
‫נשים לב שכדי לצייר את הכדור כל מה שאנו צריכים לעשות זה לקרוא ל ‪ onDraw‬של‬
‫‪.BallView‬‬
:‫ בואו נראה מה יצא‬.BallView - ‫ של הכדור‬VIEW‫ ואת ה‬PingPongView - ‫ של השולחן‬VIEW‫עד כה יצרנו את ה‬
-‫ את ה‬,‫ של המשחק‬View -‫ להגדיר את ה‬,‫ מה שאנחנו צריכים זה להגדיר את הכדור‬.‫נחבר את כל החלקים יחדיו‬
.‫ הראשי שלה‬View -‫ שלנו את ה‬Activity -‫ לתת ל‬,‫של הכדור ולבסוף‬View
:‫ תראה כך‬PingPong ‫המחלקה‬
public class PingPong extends Activity {
private PingPongView gameView;
private Ball gameBall;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
gameBall = new Ball(100, 100); // )‫ (עדיין לא מציור‬100 100 ‫אנחנו יוצרים כדור חדש במיקום‬
gameView = new PingPongView(this); // ‫ הראשי שלנו‬VIEW ‫מאתחלים את ה‬
BallView ballView = new BallView(this, gameBall); // ‫מאתחלים את מחלקת הציור של‬
‫הכדור‬
gameView.setBallView(ballView); // ‫נקבע את כדור המשחק ככדור שיצרנו שורה למעלה‬
setContentView(gameView); // ‫נקבע את תוכן הפעילות שלנו כשולחן המשחק‬
}
}
‫אם עשינו הכל‬
‫נכון‪ ,‬נקבל את‬
‫המסך הבא‬
‫באימולטור‪.‬‬
‫כמו שאנחנו‬
‫רואים‪ ,‬יש לנו‬
‫כדור!‬
‫לפני שנמשיך‪ ,‬בואו נדבר קצת על הרצת שני קטעי קוד במקביל‪.‬‬
‫בתכנות בד"כ‪ ,‬וב‪ JAVA‬בפרט‪ ,‬שורות הקוד של תהליך מסוים‬
‫מתבצעות באופן סידרתי‪ ,‬כלומר‪ ,‬עד ששורה ‪ 1‬לא מתבצעת במלואה‪,‬‬
‫לא תתבצע שורה ‪ ,2‬וכך הלאה‪.‬‬
‫אך היינו רוצים לבצע כמה פעולות במקביל בכל זאת‪ ,‬בכדי שתהליך‬
‫מסוים יתנהל "חלק" בלי לחכות לתהליכים אחרים‪.‬‬
‫ב‪ ,JAVA‬נעשה זאת בעזרת נימים (‪.)THREADS‬‬
‫באופן כללי‪ ,‬בכל מערכת הפעלה מודרנית רצות כל הזמן הרבה‬
‫תוכניות שלכל אחת מוקצה זכרון משלה וכל אחת למעשה מתפקדת‬
‫כתוכנית עצמאית‪ .‬כל תוכנית כזאת נקראת ‪.PROCESS‬‬
‫‪ PROCESS‬מסוים יכול להריץ כמה ‪ THREAD‬ים‪ ,‬ש"מתחלקים"‬
‫בזכרון של ה‪ PROCESS‬שיצר אותם‪.‬‬
‫‪)Process‬או תהליך) – כל תוכנית מחשב שמופעלת על גבי המחשב שלנו מתבצעת ב‪ Process -‬נפרד‪Process .‬מכיל‬
‫את קוד התוכנית המהודר‪ ,‬ומועלה לזיכרון המחשב ע"י מערכת ההפעלה ברגע שתוכנית המחשב מופעלת‪ ,‬על מנת‬
‫שהמעבד יוכל לבצע את ההוראות המופיעות בו‪ .‬רוב מערכות ההפעלה היום מסוגלות להפעיל מספר רב של תהליכים‬
‫במקביל‪ .‬למשל הדפדפן שהפעלתם כדי לקרוא את השיעור שלנו רץ בתוך תהליך משלו‪ ,‬במקביל להרבה תהליכים‬
‫אחרים‪ ,‬כמו לדוגמא מעבד תמלילים‪ ,‬מחשבון‪ ,‬תהליכי רקע של מערכת ההפעלה או כל תוכנה אחרת שפתוחה לכם‬
‫כעת‪.‬‬
‫‪ - Thread‬זהו קטע קוד הבסיסי ביותר שמערכת ההפעלה מסוגלת להריץ כיחידת קוד עצמאית‪ .‬כל ‪ Process‬יכול‬
‫להכיל ‪ Thread‬אחד או יותר‪.‬‬
‫ההבדל המשמעותי בין ‪ Process‬ל‪ Thread -‬הוא שכברירת מחדל‪ ,‬לכל תהליך יש זיכרון נפרד‪ ,‬עם משתנים משלו‪,‬‬
‫שתהליכים אחרים לא רואים ולא יכולים לגשת אליהם (ללא שיתוף מיוחד)‪ ,‬בעוד שכל ה‪Thread- -‬ים שרצים בתוך‬
‫תהליך מסויים חולקים כברירת מחדל את אותו הזיכרון והמשתנים‪ ,‬ויכולים לגשת ולשנות ערכים של משתנים של‬
‫‪Thread‬ים אחרים‪.‬‬‫נושא ריבוי התהליכים וה‪Thread- -‬ים הוא אחד מהנושאים המורכבים ביותר בתחום פיתוח התוכנה‪ ,‬בשל המורכבות‬
‫הנובעת מגישה במקביל לאותו משאב‪ ,‬כתיבה וקריאה מאותה כתובת זיכרון‪ ,‬והתקלות העלולות לנבוע בשל‬
‫ההתרחשות המקרית של שני ארועים בו זמנית‪ ,‬שקורות לא תמיד‪ ,‬כמו באגים רגילים‪ ,‬אלא לעיתים רחוקות – דבר‬
‫המקשה משמעותית על איתור ותיקון הבעיה‪.‬‬
‫אנחנו נשתמש ב‪ THREAD‬אחד בלבד‪ ,‬ובאופן פשוט‪ ,‬לצורך ביצוע האנימציה‪.‬‬
‫שני שקפים אחורה‪ ,‬עצרנו כאשר יש לנו כדור בנקודה (‪)100,100‬‬
‫על המסך‪.‬‬
‫לפני שניצור את התהליך שרץ ברקע ומזיז לנו את הכדור‪,‬‬
‫נצטרך להרחיב את המחלקה הלוגית של הכדור שלנו )‪(Ball‬‬
‫ולהוסיף לה מתודה שמזיזה את הכדור על המסך‪.‬‬
‫המתודה תזיז את הכדור בכל פעם בפיקסל אחד בציר ה‪x -‬ובפיקסל אחד‬
‫בציר ה‪y, -‬בהתאם לכיוון התנועה של הכדור‪.‬‬
‫כלומר‪:‬‬
‫אם הכדור זז לכיוון ימין מטה‪ ,‬המתודה תוסיף למיקום האופקי‬
‫(משתנה ה ‪ )x‬של הכדור ‪ +1‬ולמיקום האנכי (משתנה ה ‪ (y‬של הכדור ‪.+1‬‬
‫אם הכדור זז לכיוון שמאל מטה‪ ,‬המתודה תוסיף למיקום האופקי‬
‫(משתנה ה ‪ (x‬של הכדור ‪ -1‬ולמיקום האנכי (משתנה ה ‪ )y‬של הכדור ‪.+1‬‬
‫אם הכדור זז לכיוון ימין מעלה‪ ,‬המתודה תוסיף למיקום האופקי‬
‫(משתנה ה ‪ )x‬של הכדור ‪ +1‬ולמיקום האנכי (משתנה ה ‪ (y‬של הכדור ‪.-1‬‬
‫אם הכדור זז לכיוון שמאל מעלה‪ ,‬המתודה תוסיף למיקום האופקי‬
‫(משתנה ה ‪ (x‬של הכדור ‪ -1‬ולמיקום האנכי (משתנה ה ‪ )y‬של הכדור ‪.-1‬‬
‫בנוסף לכל אלה‪ ,‬אנחנו צריכים גם לבדוק האם הכדור פגע בגבולות המסך‬
‫ואם כן‪ ,‬אז לשנות את כיוון הכדור בהתאם לאיפה שהוא פגע‪.‬‬
‫איור המחשה לתזוזה של הכדור‪:‬‬
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)
{
//constructor with arguments
this.x = startX;
this.y = startY;
this.maxWidth = screenWidth;
this.maxHeight = screenHight;
this.movingBottom = false;
this.movingRight = false;
}
:‫ עם מימוש התנועה יראה כך‬Ball ‫קוד המחלקה‬
:‫המשך קוד המחלקה‬
public int getX(){
return this.x;
}
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;
}}
‫הסבר קצר על הקוד‪:‬‬
‫המשתנים ‪ MaxWidth‬ו ‪ MaxHeight‬יחזיקו את הרוחב‬
‫והגובה של מסך המשחק‪ ,‬כלומר הגבולות‪ ,‬כך שנדע מתי הכדור‬
‫צריך לשנות את כיוון תנועתו‪.‬‬
‫בפונקצייה ‪ moveBall‬אנחנו מזיזים את הכדור בצורה‬
‫מבוקרת‪ .‬בחלק הראשון של הפונקציה אנחנו זזים בהתאם‬
‫למשתנים ‪ movingRight‬ו ‪ ,movingBottom‬ובחלק השני‬
‫בודקים אם הגענו לגבול המסך שלנו כדי לשנות כיוון אם צריך‬
‫(אם היינו מגדילים את ‪ X‬מעבר לגבול המסך הכדור היה‬
‫נעלם‪.)..‬‬
‫כעת ניצור את הנים (‪ )THREAD‬שיזיז את הכדור‪ .‬למה‬
‫לעשות את זה ב‪?THREAD‬‬
‫כאשר תהליך התזוזה של הכדור תלוי ב‪ THREAD‬הראשי‪,‬‬
‫יווצרו "לאגים"‪ ,‬כלומר תזוזת הכדור תהיה לא חלקה‪ ,‬מכיוון‬
‫שה‪ THREAD‬הראשי מבצע עוד פעולות‪.‬‬
‫ה‪ THREAD‬החדש שניצור יוקדש כולו לתנועת הכדור‪.‬‬
‫בגדול‪ ,‬מה שה‪ THREAD‬שלנו צריך לעשות הוא "להתעורר" כל‬
‫פרק זמן מסויים‪ ,‬להזיז את הכדור ואז לעדכן ה‪View -‬שלנו‬
‫שמשהו השתנה בלוגיקה ושצריך לצייר את המסך מחדש‪.‬‬
‫ניצור מחלקה חדשה בשם ‪ PingPongGame‬היורשת מהמחלקה‬
‫‪.THREAD‬‬
‫ישנן שתי דרכים ליצור נים‪ .‬דרך אחת היא עם אובייקט מסוג‬
‫‪ ,Runnable‬שבו נממש את הפונקציה )(‪.run‬‬
‫בדרך שלנו‪ ,‬שבה הרחבנו את ‪ ,THREAD‬נדרוס את הפונקציה‬
‫)(‪ run‬בתוך המחלקה שלנו‪ .‬קוד המחלקה יראה כך‪:‬‬
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 (true){
this.gameBall.moveBall();
this.gameView.postInvalidate();
{
try
PingPongGame.sleep(5);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
‫הסבר קצר על הקוד‪:‬‬
‫;)(‪this.gameBall.moveBall‬‬
‫;)(‪this.gameView.postInvalidate‬‬
‫השורה הראשונה מזיזה את הכדור (באופן לוגי‪ ,‬לא על המסך)‪,‬‬
‫והשורה השנייה אומרת ל‪ VIEW‬שיש שינוי בתצוגה ויש צורך לעדכן‪ .‬אם לא‬
‫היינו בתוך ‪ THREAD‬היינו משתמשים בשיטה )(‪.invalidate‬‬
‫{‪Try‬‬
‫;)‪PingPongGame.sleep(5‬‬
‫}…{)…)‪}catch‬‬
‫השורה הזאת אומרת לנים "לישון" ל‪ 5‬מילי שניות‪ .‬הסיבה ל‪ TRY‬וה‪CATCH‬‬
‫היא שה"שינה" המאולצת הזאת לא תמיד מצליחה‪ ,‬אז אנו רוצים לטפל במקרים‬
‫שהיא לא‪ ,‬ע"י תפיסת החריגה שהיא זורקת‪ .‬חריגות – בשקפים על ‪.JAVA‬‬
‫אם לא היינו אומרים לנים לישון אחרי כל תזוזה‪ ,‬הכדור היה זז כל כך מהר שלא‬
‫היינו מבחינים בו כלל‪.‬‬
.‫ ונראה מה קיבלנו‬,‫ שוב נחבר את כל מה שיצרנו עד כה‬,‫כעת‬
:‫ (הפעילות שלנו) שתראה כך‬PingPong ‫נשכתב את המחלקה‬
public class PingPong extends Activity {
private PingPongView gameView;
private Ball gameBall;
private PingPongGame gameThread;
@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();
gameBall = new Ball(screenWidth / 2, screenHeight / 2, screenWidth,
screenHeight);
BallView ballView = new BallView(this, gameBall);
this.gameView = new PingPongView(this);
gameView.setBallView(ballView);
setContentView(gameView);
gameThread = new PingPongGame(gameBall, gameView);
gameThread.start();
}
{
:‫הסבר קצר על הקוד‬
‫השורות האלו‬
int screenWidth = this.getWindowManager().getDefaultDisplay().getWidth();
int screenHeight = this.getWindowManager().getDefaultDisplay().getHeight();
‫ כדי שנוכל לדעת מתי‬,‫נותנות לנו את הגובה והרוחב של המסך‬
.‫לשנות כיוון לכדור‬
‫ מתחילה את ריצת‬gameThread.start() ‫השורה‬
.(‫הנים)במקביל לפעילות שרצה‬
‫אוקי‪ ..‬אז מה יש לנו עד כה?‬
‫יש לנו כדור‪ ,‬שזז לו על המסך בנחת‪ ..‬איפה נכנס כאן‬
‫המשתמש?‬
‫אנחנו צריכים מנגנון כלשהו שידע מתי המשתמש מבצע פעולה‬
‫כלשהי (למשל לחיצה על המסך)‪ ,‬ושאותו מנגנון יאפשר לנו‬
‫להגיב על אותן פעולות‪.‬‬
‫אותן פעולות של המשתמש נקראות אירועים (‪,)EVENTS‬‬
‫וניהולן נקרא טיפול באירועים (‪.)EVENT HANDLING‬‬
‫להלן כמה דוגמאות לאירועים ב‪:ANDROID‬‬
‫אירועי מגע (‪ – )TOUCH‬כאשר ישנה נגיעה במסך‪.‬‬
‫אירועי לחיצה (‪ – )CLICK‬כאשר ישנה לחיצה על כפתור או‬
‫רכיב אחר אשר לחיצה מוגדרת עבורו‪.‬‬
‫אירועי שינוי פוקוס (‪ – )FOCUS CHANGE‬כאשר פוקוס‬
‫התוכנית עובר מרכיב אחד לאחר במסך‪ ,‬או ממסך אחד לאחר‪.‬‬
‫אירועי מקש (‪ – )KEY‬כאשר ישנה לחיצה על מקש מקלדת או‬
‫כפתור חומרה (כמו כפתור הכיבוי למשל)‪.‬‬
‫בכדי "לתפוס" את האירועים האלו כאשר הם מתרחשים‪ ,‬אנו‬
‫נשתמש במאזינים (‪.)LISTENERS‬‬
‫מאזין הוא אובייקט ש"מחכה" לאירוע מסוים‪ ,‬וכאשר האירוע‬
‫הזה מתרחש הוא מפעיל שיטה מסוימת‪.‬‬
‫לכל אירוע יש מאזין ספציפי לו‪ .‬למשל‪ ,‬בכדי להאזין ללחיצה על‬
‫כפתור הכיבוי‪ ,‬נשתמש במאזין מסוג ‪.OnKeyListener‬‬
‫המאזין יקלוט כל אירוע מהסוג הזה ויריץ את השיטה ‪onKey‬‬
‫עבור כל אירוע כזה שהוא קולט‪ .‬ע"י מימוש השיטה ‪onKey‬‬
‫אנחנו בעצם מגיבים לאירוע איך שנבחר‪ .‬למשל‪ ,‬נוכל להגיב על‬
‫לחיצת כפתור הכיבוי בהודעה על המסך‪.‬‬
‫את המאזין נרשום אצלנו בתוכנית כדי שיתחיל להאזין‪ ,‬בד"כ ע"י‬
‫שיטת ‪ .set‬למשל‪ ,‬אם נרצה להוסיף לכפתור ‪ b‬מסוים מאזין ללחיצה‬
‫זה יראה כך‪:‬‬
‫{)(‪b.setOnClickListener(new OnClickListener‬‬
‫{)‪onClick(..‬‬
‫‪// implementation‬‬
‫}‬
‫;)}‬
‫כמו שרואים‪ ,‬ניתן לממש את אובייקט המאזין אנונימית ולא‬
‫במחלקה משלו‪ ,‬למרות שגם זה אפשרי‪.‬‬
‫השיטה שנממש תקבל בד"כ ארגומנטים שיכילו מידע על האירוע‪,‬‬
‫כמו נקודות הלחיצה‪ ,‬עוצמת הלחיצה‪ ,‬משך זמן הלחיצה וכדומה‪.‬‬
‫טוב‪ ,‬כעת שאנחנו יודעים מהו אירוע‪ ,‬אפשר להמשיך לפתח את‬
‫המשחק‪.‬‬
‫ראשית‪ ,‬נרחיב את המחלקות ‪ Ball‬ו ‪ Paddle‬כך שירשו‬
‫ממחלקה אחת שתהווה בסיס לכל אובייקט שיכול לזוז אצלנו‪.‬‬
‫נראה למחלקת האב הזאת ‪ ,MoveableObject‬ומימושה יהיה‬
‫כזה שיכריח כל מחלקת בת שלה לממש את שיטות המחזירות‬
‫את גבולות האובייקט שאותו היא מממשת‪ .‬נעשה זאת בעזרת‬
‫שיטות אבסטרקטיות‪.‬‬
‫עוד על מחלקות אבסטרקטיות תמצאו בשקפים בנושא הורשה‬
‫ופולימורפיזם ב‪...JAVA‬‬
public abstract class MovableObject
{
protected int x; //current horizontal position of the center of the object
protected int y; //current vertical position of the center of the object
public int getX() { return x; }
public int getY() { return y; }
public abstract int getLeft();
//should return the right border of the object
public abstract int getRight();
//should return the top border of the object
public abstract int getTop();
//should return the bottom border of the object
public abstract int getBottom();
//This method should move the object
public abstract void move();
}
Ball ‫ נשכתב שוב (בפעם האחרונה) את המחלקה‬,MovableObject ‫אחרי שהוספנו את המחלקה‬
:Paddle ‫ואת המחלקה‬
public class Ball extends MovableObject
{
private final int RESTART_X,RESTART_Y; //‫נקודת ההתחלה של המשחק‬
private int maxWidth,maxHeight;//‫גבולות המסך‬
private boolean movingRight,movingBottom; //‫כיוון התנועה‬
public final int RADIUS = 10; //‫רדיוס הכדור‬
}
public Ball (int startX, int startY, int screenWidth, int screenHight){
maxWidth = screenWidth;
maxHeight = screenHight;
RESTART_X = startX;
RESTART_Y = startY;
restart();
public void restart(){
x = RESTART_X;
y = RESTART_Y;
movingBottom = false;
movingRight = false;
}
public int getLeft(){ return x - RADIUS; }
public int getRight(){ return x + RADIUS; }// MovableObject ‫מימוש השיטות האבסטרקטיות שירשנו מ‬
public int getTop(){ return y - RADIUS;}
public int getBottom(){ return y + RADIUS;{
public void move(){
if (movingRight) x++;
else x--;
if (movingBottom) y++;
else y--;
if (x >= maxWidth) movingRight = false;
else if (x <= 1) movingRight = true;
movingBottom = false;
if (y >= maxHeight)
else if (y <= 1) movingBottom = true;
}
public void bounceUp(){
movingBottom = false;
}
}
:‫ המייצגת את המחבט שלנו תראה כך‬Paddle ‫ המחלקה‬,‫בדומה‬
public class Paddle extends MovableObject
{
private final int screenWidth;
public final int HEIGHT = 10;
private final int WIDTH = 50;
private float paddleDestination;
public Paddle(int startX, int startY, int newScreenWidth){
x = startX;
y = startY;
screenWidth = newScreenWidth;
paddleDestination = screenWidth / 2;
}
.‫ ויש לנו את רוחב המסך בכדי לשלוט בתנועת המחבט‬,‫ למחבט יש גובה ורוחב‬,‫כאמור‬
:MovableObject‫נממש את השיטות שירשנו מ‬
public synchronized int getLeft(){ return x; }
public synchronized int getRight(){ return x + WIDTH; }
public synchronized int getTop() { return y; }
public synchronized int getBottom() {return y + HEIGHT;}
:‫ועוד כמה שיטות שיעזרו לנו עוד מעט‬
public synchronized int getPaddleWidth(){ return WIDTH; }
public synchronized int getMiddle(){ return x + WIDTH/2; }
public synchronized float getPaddleDestination(){ return paddleDestination; }
public synchronized void setPaddleDestination(float newDestination){
paddleDestination = newDestination;
}
:‫ נטפל בתנועת המחבט‬,‫וכמובן‬
public synchronized void moveRight(){
if (screenWidth > (getRight()+1)) x++;
else x = screenWidth - WIDTH;
}
public synchronized void moveLeft(){
if ((getLeft()-1) > 0) x--;
else x = 0;
}
public synchronized void move(){
float destination;
destination = getPaddleDestination();
moveRight();
if (getMiddle() < destination)
moveLeft();
else if (getMiddle() > destination)
}
}
:‫ בדומה לכדור‬,PaddleView ‫ בעזרת המחלקה‬,‫כעת אפשר להציג את המחבט שלנו על המסך‬
public class PaddleView extends View
{
private Paddle paddle; //our paddle to be drawn
private Paint padlePaint; //the paint to paint the paddle with
public PaddleView(Context context, Paddle newPaddle){
super(context);
this.paddle = newPaddle;
}
private Paint getPaddlePaint(){
{
if (padlePaint == null)
padlePaint = new Paint();
padlePaint.setStrokeWidth(2);
padlePaint.setColor(Color.BLUE);
}
return padlePaint;
}
@Override
public void onDraw(Canvas canvas){
int left,right,top,bottom;
left = paddle.getLeft();
right = paddle.getRight();
top = paddle.getTop();
bottom = paddle.getBottom();
canvas.drawRect(left, top, right, bottom, getPaddlePaint());
}
}
public class PingPongView extends View
{
private BallView ballView; //ball view
private PaddleView paddleView; // paddle view
:‫ כך‬,‫ הראשי שלנו את המחבט‬View‫ נוסיף ל‬,‫כעת‬
public PingPongView(Context context){
super(context);
}
public void setViews(BallView ballView,PaddleView paddleView){
this.ballView = ballView;
this.paddleView = paddleView;
}
@Override
public void onDraw(Canvas canvas){
this.ballView.onDraw(canvas);
this.paddleView.onDraw(canvas);
}
}
!‫ הזזת המחבט שלנו ממש‬:‫ועכשיו הגענו לקטע המעניין‬
,‫ נפרד‬Thread -‫אנחנו נבצע זאת ב‬
– ‫כדי לאפשר את התזוזה של המחבט באמת במקביל לארועים אחרים שקורים במשחק שלנו‬
.‫כמו תנועת הכדור‬
:Thread -‫ היורשת מ‬PaddleMover – ‫לצורך כך נוסיף מחלקה חדשה‬
public class PaddleMover extends Thread
{
private Paddle gamePaddle; //holds a reference to the paddle
private PingPongView gameView; //holds a reference to the
main view
public PaddleMover(Paddle thePaddle, PingPongView
mainView){
gamePaddle = thePaddle;
gameView = mainView;
}
@Override
{ public void run()
while (1 < 2){
if (gamePaddle.getMiddle() !=
gamePaddle.getPaddleDestination()){
gamePaddle.move();
gameView.postInvalidate();
}
try{
PaddleMover.sleep(3);
{
}catch (InterruptedException e)
e.printStackTrace();
}
}
}
}
‫במקרה שלנו אנו נרצה להאזין לארועים של נגיעת המשתמש במסך‪ .‬באנדרואיד‪ ,‬המחלקה‬
‫‪View‬מממשת את ‪ onTouchEvent,‬כלומר נוכל לזהות לחיצה על כל רכיב שלנו המממש‬
‫את ‪) View‬כמו המחבט או הכדור)‪.‬‬
‫גם המחלקה ‪ Activity‬מממשת את ‪ onTouchEvent‬כאשר המשמעות היא האזנה‬
‫ללחיצת בכל מקום באפליקציה שלנו‪ ,‬ולא על רכיב מסויים‪.‬‬
‫אנו נממש את ‪ onTouchEvent‬בתוך ה‪Activity -‬שלנו‪ ,‬בכך ש"נדרוס" אותה ונגדיר לה‬
‫מימוש משלנו‪:‬‬
‫‪public class PingPong extends Activity‬‬
‫{‬
‫;‪private PingPongView gameView‬‬
‫;‪private Ball gameBall‬‬
‫;‪private Paddle gamePaddle‬‬
‫;‪private PingPongGame gameThread‬‬
‫;‪private PaddleMover paddleMoverThread‬‬
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
int screenWidth,screenHeight;
BallView ballView;
PaddleView paddleView;
screenWidth = this.getWindowManager().getDefaultDisplay().getWidth();
screenHeight = this.getWindowManager().getDefaultDisplay().getHeight();
gameBall = new Ball(screenWidth / 2, screenHeight / 2, screenWidth, screenHeight);
ballView = new BallView(this, gameBall);
gamePaddle = new Paddle(screenWidth / 2, (int)(screenHeight * 0.85), screenWidth);
paddleView = new PaddleView(this, gamePaddle);
this.gameView = new PingPongView(this);
gameView.setViews(ballView,paddleView);
setContentView(gameView);
gameThread = new PingPongGame(gameBall, gamePaddle, gameView);
gameThread.start();
paddleMoverThread = new PaddleMover(gamePaddle, gameView);
paddleMoverThread.start();
}
@Override
public boolean onTouchEvent(MotionEvent event){
float destination;
destination = event.getX();
gamePaddle.setPaddleDestination(destination);
return true;
}
}
‫כעת כל מה שנותר לנו הוא לעדכן את התהליכון הראשי של המשחק המזיז את הכדור ולהפעיל‬
:‫גם את המחבט‬
public class PingPongGame extends Thread
{
private Ball gameBall; //holds a reference to the ball
private Paddle gamePaddle; //holds a reference to the paddle
private PingPongView gameView; //holds a reference to the
main view
public PingPongGame(Ball theBall, Paddle thePaddle,
PingPongView mainView){
gameBall = theBall;
gamePaddle = thePaddle;
gameView = mainView;
}
@Override
public void run(){
while (1 < 2)
{
gameBall.move();
checkHitPaddle();
gameView.postInvalidate();
{
try
PingPongGame.sleep(5);
{ }catch (InterruptedException e)
e.printStackTrace();
}
}
}
private void checkHitPaddle(){
if (gameBall.getBottom() >= gamePaddle.getTop())
{
if ((gameBall.getRight() > gamePaddle.getLeft()) &&
(gameBall.getLeft() < gamePaddle.getRight())){
//The ball had hit the paddle - so it should be bounced
gameBall.bounceUp();
}else{
//The ball had missed the paddle - player looses - restart the game
gameBall.restart();
}
}
}
}
‫זהו!‬
‫כל מה שנותר זה להריץ‪ ..‬מקווה שנהנתם‪ ,‬ולמדתם‬
‫משהו‪.‬‬
‫עירן‪.‬‬