สุดยอดคู่มือสำหรับการประมวลผลภาษา ตอนที่ II: การสร้างเกมอย่างง่าย

เผยแพร่แล้ว: 2022-03-11

นี่เป็นส่วนที่สองของคู่มือขั้นสูงสุดสำหรับภาษาการประมวลผล ในส่วนแรก ฉันได้ให้คำแนะนำเบื้องต้นเกี่ยวกับภาษาการประมวลผล ขั้นตอนต่อไปสำหรับคุณในการเรียนรู้การประมวลผลคือการเขียนโปรแกรมเชิงปฏิบัติมากขึ้น

ในบทความนี้ ผมจะแสดงให้คุณเห็นถึงวิธีการใช้การประมวลผลเพื่อติดตั้งเกมของคุณเองทีละขั้นตอน แต่ละขั้นตอนจะอธิบายโดยละเอียด จากนั้นเราจะพอร์ตเกมไปยังเว็บ

สร้างเกมง่ายๆ ด้วยภาษาการประมวลผล

ก่อนที่เราจะเริ่มบทช่วยสอนการประมวลผล นี่คือรหัสของแบบฝึกหัดโลโก้ดีวีดีจากส่วนก่อนหน้า หากคุณมีคำถามใด ๆ โปรดแสดงความคิดเห็น

บทช่วยสอนการประมวลผล: เกมง่ายๆ

เกมที่เราจะสร้างในบทช่วยสอนการประมวลผลนี้เป็นการผสมผสานระหว่าง Flappy Bird, Pong และ Brick Breaker เหตุผลที่ฉันเลือกเกมแบบนี้ก็คือมีแนวคิดส่วนใหญ่ที่ผู้เริ่มต้นต้องดิ้นรนเมื่อเรียนรู้การพัฒนาเกม อิงจากประสบการณ์ของฉันเมื่อครั้งเป็นผู้ช่วยสอน ช่วยให้โปรแกรมเมอร์ใหม่เรียนรู้วิธีใช้การประมวลผล แนวคิดเหล่านี้รวมถึงแรงโน้มถ่วง การชน การรักษาคะแนน การจัดการหน้าจอต่างๆ และการโต้ตอบของแป้นพิมพ์/เมาส์ เครื่องปัดโป่งมีครบทุกอย่าง

เล่นเกมเลย!

หากไม่ใช้แนวคิดการเขียนโปรแกรมเชิงวัตถุ (OOP) มันไม่ง่ายเลยที่จะสร้างเกมที่ซับซ้อน เช่น เกมแพลตฟอร์มที่มีหลายระดับ ผู้เล่น เอนทิตี ฯลฯ เมื่อเราก้าวไปข้างหน้า คุณจะเห็นว่าโค้ดมีความซับซ้อนอย่างรวดเร็วเพียงใด ฉันพยายามอย่างเต็มที่เพื่อให้บทแนะนำการประมวลผลนี้เป็นระเบียบและเรียบง่าย

ฉันแนะนำให้คุณติดตามบทความ คว้าโค้ดทั้งหมด เล่นด้วยตัวเอง เริ่มคิดเกี่ยวกับเกมของคุณเองโดยเร็วที่สุด และเริ่มใช้งาน

เริ่มกันเลย

ตึกเครื่องปัดโป่ง

ขั้นตอนการสอนการประมวลผล #1: เริ่มต้นและจัดการหน้าจอต่างๆ

ขั้นตอนแรกคือการเริ่มต้นโครงการของเรา สำหรับผู้เริ่มต้น เราจะเขียนการตั้งค่าและวาดบล็อคตามปกติ ไม่มีอะไรแปลกใหม่หรือแปลกใหม่ จากนั้นเราจะจัดการกับหน้าจอต่างๆ (หน้าจอเริ่มต้น หน้าจอเกม เกมโอเวอร์หน้าจอ ฯลฯ) เลยเกิดคำถามว่า ทำอย่างไรให้ Process แสดงหน้าที่ถูกต้องในเวลาที่ถูกต้อง ?

การทำภารกิจนี้ให้สำเร็จนั้นค่อนข้างง่าย เราจะมีตัวแปรส่วนกลางที่เก็บข้อมูลของหน้าจอที่ใช้งานอยู่ในปัจจุบัน จากนั้นเราวาดเนื้อหาของหน้าจอที่ถูกต้องขึ้นอยู่กับตัวแปร ในบล็อกการวาด เราจะมีคำสั่ง if ที่ตรวจสอบตัวแปรและแสดงเนื้อหาของหน้าจอตามลำดับ เมื่อใดก็ตามที่เราต้องการเปลี่ยนหน้าจอ เราจะเปลี่ยนตัวแปรนั้นเป็นตัวระบุหน้าจอที่เราต้องการให้แสดง จากที่กล่าวมานี่คือสิ่งที่รหัสโครงกระดูกของเราดูเหมือน:

 /********* VARIABLES *********/ // We control which screen is active by settings / updating // gameScreen variable. We display the correct screen according // to the value of this variable. // // 0: Initial Screen // 1: Game Screen // 2: Game-over Screen int gameScreen = 0; /********* SETUP BLOCK *********/ void setup() { size(500, 500); } /********* DRAW BLOCK *********/ void draw() { // Display the contents of the current screen if (gameScreen == 0) { initScreen(); } else if (gameScreen == 1) { gameScreen(); } else if (gameScreen == 2) { gameOverScreen(); } } /********* SCREEN CONTENTS *********/ void initScreen() { // codes of initial screen } void gameScreen() { // codes of game screen } void gameOverScreen() { // codes for game over screen } /********* INPUTS *********/ public void mousePressed() { // if we are on the initial screen when clicked, start the game if (gameScreen==0) { startGame(); } } /********* OTHER FUNCTIONS *********/ // This method sets the necessary variables to start the game void startGame() { gameScreen=1; }

นี้อาจดูน่ากลัวในตอนแรก แต่ทั้งหมดที่เราทำคือสร้างโครงสร้างพื้นฐานและแยกส่วนต่างๆ ที่มีบล็อกความคิดเห็น

อย่างที่คุณเห็น เราได้กำหนดวิธีการที่แตกต่างกันสำหรับแต่ละหน้าจอที่จะแสดง ในบล็อกการวาดของเรา เราเพียงแค่ตรวจสอบค่าของตัวแปร gameScreen และเรียกวิธีการที่เกี่ยวข้อง

ในส่วน void mousePressed(){...} เรากำลังฟังการคลิกเมาส์ และหากหน้าจอที่ใช้งานอยู่เป็น 0 หน้าจอเริ่มต้น เราจะเรียก startGame() ซึ่งเริ่มเกมตามที่คุณคาดหวัง บรรทัดแรกของวิธีนี้เปลี่ยนตัวแปร gameScreen เป็น 1 หน้าจอเกม

หากเข้าใจแล้ว ขั้นตอนต่อไปคือการใช้หน้าจอเริ่มต้นของเรา ในการดำเนินการดังกล่าว เราจะทำการแก้ไข initScreen() นี่มันไป:

 void initScreen() { background(0); textAlign(CENTER); text("Click to start", height/2, width/2); }

ตอนนี้หน้าจอเริ่มต้นของเรามีพื้นหลังสีดำและข้อความธรรมดา "คลิกเพื่อเริ่ม" ซึ่งอยู่ตรงกลางและจัดชิดตรงกลาง แต่เมื่อเราคลิกไม่มีอะไรเกิดขึ้น เรายังไม่ได้ระบุเนื้อหาใด ๆ สำหรับหน้าจอเกมของเรา เมธอด gameScreen() ไม่มีอะไรอยู่ในนั้น ดังนั้นเราจึงไม่ครอบคลุมเนื้อหาก่อนหน้าที่ดึงมาจากหน้าจอสุดท้าย (ข้อความ) โดยมี background() เป็นบรรทัดแรกของการวาด นั่นเป็นสาเหตุที่ข้อความยังคงอยู่ที่นั่นแม้ว่าจะไม่มีการเรียกบรรทัด text() อีกต่อไป (เช่นเดียวกับตัวอย่างลูกบอลที่กำลังเคลื่อนที่จากส่วนสุดท้ายซึ่งทิ้งร่องรอยไว้เบื้องหลัง) พื้นหลังยังคงเป็นสีดำด้วยเหตุผลเดียวกัน มาเริ่มใช้งานหน้าจอเกมกันเลย

 void gameScreen() { background(255); }

หลังจากการเปลี่ยนแปลงนี้ คุณจะสังเกตเห็นว่าพื้นหลังเปลี่ยนเป็นสีขาวและข้อความหายไป

ขั้นตอนการสอนการประมวลผล #2: การสร้างลูกบอลและการใช้แรงโน้มถ่วง

ตอนนี้เราจะเริ่มทำงานบนหน้าจอเกม ก่อนอื่นเราจะสร้างลูกบอลของเรา เราควรกำหนดตัวแปรสำหรับพิกัด สี และขนาด เนื่องจากเราอาจต้องการเปลี่ยนค่าเหล่านั้นในภายหลัง ตัวอย่างเช่น หากเราต้องการเพิ่มขนาดของลูกบอลในขณะที่ผู้เล่นทำคะแนนให้สูงขึ้นเพื่อให้เกมนั้นยากขึ้น เราจะต้องเปลี่ยนขนาดจึงควรเป็นตัวแปร เราจะกำหนดความเร็วของลูกบอลด้วยเช่นกัน หลังจากที่เราใช้แรงโน้มถ่วง

ขั้นแรก ให้เพิ่มสิ่งต่อไปนี้:

 ... int ballX, ballY; int ballSize = 20; int ballColor = color(0); ... void setup() { ... ballX=width/4; ballY=height/5; } ... void gameScreen() { ... drawBall(); } ... void drawBall() { fill(ballColor); ellipse(ballX, ballY, ballSize, ballSize); }

เรากำหนดพิกัดเป็นตัวแปรส่วนกลาง สร้างวิธีการดึงลูกบอล เรียกจากวิธี gameScreen สิ่งเดียวที่ต้องใส่ใจในที่นี้คือเรา เริ่มต้น พิกัด แต่เรากำหนดไว้ใน setup() เหตุผลที่เราทำนั่นคือเราต้องการให้บอลเริ่มที่หนึ่งในสี่จากด้านซ้ายและหนึ่งในห้าจากด้านบน ไม่มีเหตุผลเฉพาะเจาะจงที่เราต้องการ แต่นั่นเป็นจุดที่ดีสำหรับลูกบอลที่จะเริ่มต้น ดังนั้นเราจึงจำเป็นต้องได้ width และ height ของร่างแบบไดนามิก ขนาดร่างถูกกำหนดใน setup() หลังจากบรรทัดแรก width และ height ไม่ได้ถูกตั้งค่าก่อนการรัน setup() นั่นเป็นสาเหตุที่เราไม่สามารถทำสิ่งนี้ได้หากเรากำหนดตัวแปรไว้ด้านบน

แรงโน้มถ่วง

ตอนนี้การใช้แรงโน้มถ่วงเป็นส่วนที่ง่าย มีเพียงไม่กี่เทคนิค นี่คือการใช้งานก่อน:

 ... float gravity = 1; float ballSpeedVert = 0; ... void gameScreen() { ... applyGravity(); keepInScreen(); } ... void applyGravity() { ballSpeedVert += gravity; ballY += ballSpeedVert; } void makeBounceBottom(float surface) { ballY = surface-(ballSize/2); ballSpeedVert*=-1; } void makeBounceTop(float surface) { ballY = surface+(ballSize/2); ballSpeedVert*=-1; } // keep ball in the screen void keepInScreen() { // ball hits floor if (ballY+(ballSize/2) > height) { makeBounceBottom(height); } // ball hits ceiling if (ballY-(ballSize/2) < 0) { makeBounceTop(0); } }

และผลลัพธ์ก็คือ:

ลูกบอลกระดอนอย่างไม่มีกำหนดด้วยแรงโน้มถ่วงเทียม

ถือม้าของคุณนักฟิสิกส์ ฉันรู้ว่าแรงโน้มถ่วงในชีวิตจริงไม่ได้ทำงานแบบนั้น แทนที่จะเป็นกระบวนการแอนิเมชั่นมากกว่าสิ่งใด ตัวแปรที่เรากำหนดเป็น gravity เป็นเพียงค่าตัวเลข— float เพื่อให้เราสามารถใช้ค่าทศนิยม ไม่ใช่แค่จำนวนเต็ม—ที่เราเพิ่มใน ballSpeedVert ในทุกลูป และ ballSpeedVert คือความเร็วแนวตั้งของลูกบอล ซึ่งเพิ่มเข้ากับพิกัด Y ของลูกบอล ( ballY ) ในแต่ละลูป เราดูพิกัดของลูกบอลและตรวจสอบให้แน่ใจว่าลูกบอลอยู่ในหน้าจอ ถ้าเราไม่ทำ ลูกบอลจะตกสู่อนันต์ สำหรับตอนนี้ ลูกบอลของเราเคลื่อนที่ในแนวตั้งเท่านั้น ดังนั้นเราจึงดูขอบพื้นและเพดานของหน้าจอ ด้วยวิธี keepInScreen() เราตรวจสอบว่า ballY ( + รัศมี) น้อยกว่า height และในทำนองเดียวกัน ballY ( - รัศมี) มากกว่า 0 หากเงื่อนไขไม่เป็นไปตามนั้น เราจะทำการเด้งลูกบอล (จากด้านล่างหรือด้านบน) ด้วย makeBounceBottom() และ makeBounceTop() ในการทำให้ลูกบอลกระดอน เราเพียงแค่ย้ายลูกบอลไปยังตำแหน่งที่แน่นอนซึ่งมันจะต้องเด้งและคูณความเร็วแนวตั้ง ( ballSpeedVert ) ด้วย -1 (การคูณด้วย -1 จะเปลี่ยนเครื่องหมาย) เมื่อค่าความเร็วมีเครื่องหมายลบ บวกพิกัด Y ความเร็วจะกลายเป็น ballY + (-ballSpeedVert) ซึ่งก็คือ ballY - ballSpeedVert ดังนั้นลูกบอลจึงเปลี่ยนทิศทางทันทีด้วยความเร็วเท่าเดิม จากนั้นเมื่อเราเพิ่ม gravity ให้กับ ballSpeedVert และ ballSpeedVert มีค่าลบ มันเริ่มเข้าใกล้ 0 ในที่สุดก็กลายเป็น 0 และเริ่มเพิ่มขึ้นอีกครั้ง ที่ทำให้ลูกสูงขึ้น สูงขึ้นช้าลง หยุดและเริ่มล้ม

ลูกบอลกระดอนบนแร็กเกตอย่างไม่มีกำหนด

มีปัญหากับกระบวนการแอนิเมชั่นของเรา ลูกบอลยังคงกระเด้งอยู่ หากเป็นสถานการณ์จริง ลูกบอลจะต้องเผชิญกับแรงต้านของอากาศและการเสียดสีทุกครั้งที่สัมผัสพื้นผิว นั่นคือพฤติกรรมที่เราต้องการสำหรับกระบวนการสร้างแอนิเมชั่นเกมของเรา ดังนั้นการปรับใช้สิ่งนี้จึงเป็นเรื่องง่าย เราเพิ่มสิ่งต่อไปนี้:

 ... float airfriction = 0.0001; float friction = 0.1; ... void applyGravity() { ... ballSpeedVert -= (ballSpeedVert * airfriction); } void makeBounceBottom(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); } void makeBounceTop(int surface) { ... ballSpeedVert -= (ballSpeedVert * friction); }

และตอนนี้กระบวนการแอนิเมชั่นของเราทำให้เกิดสิ่งนี้:

ลูกบอลกระดอนแต่มาหยุดเนื่องจากการเสียดสี

ตามชื่อของมัน friction คือการเสียดสีพื้นผิว และแรง airfriction ทานอากาศคือการเสียดสีของอากาศ เห็นได้ชัดว่าต้อง friction ทุกครั้งที่ลูกบอลสัมผัสพื้นผิวใดๆ airfriction ต้องใช้อย่างต่อเนื่อง นั่นคือสิ่งที่เราทำ applyGravity() ทำงานในแต่ละลูป ดังนั้นเราจึงลบ 0.0001 เปอร์เซ็นต์ของมูลค่าปัจจุบันออกจาก ballSpeedVert ในทุกลูป makeBounceBottom() และ makeBounceTop() จะทำงานเมื่อลูกบอลสัมผัสพื้นผิวใดๆ ดังนั้นในวิธีการเหล่านั้น เราทำสิ่งเดียวกัน เฉพาะครั้งนี้ด้วย friction

ขั้นตอนการสอนการประมวลผล #3: การสร้างแร็กเกต

ตอนนี้เราต้องการไม้แร็กเก็ตเพื่อให้ลูกบอลกระดอนไป เราควรจะควบคุมแร็กเกต มาทำให้สามารถควบคุมได้ด้วยเมาส์ นี่คือรหัส:

 ... color racketColor = color(0); float racketWidth = 100; float racketHeight = 10; ... void gameScreen() { ... drawRacket(); ... } ... void drawRacket(){ fill(racketColor); rectMode(CENTER); rect(mouseX, mouseY, racketWidth, racketHeight); }

เรากำหนดสี ความกว้าง และความสูงของแร็กเกตเป็นตัวแปรส่วนกลาง เราอาจต้องการให้เปลี่ยนระหว่างการเล่นเกม เราใช้เมธอด drawRacket() ซึ่งทำตามชื่อของมัน เราตั้งค่า rectMode ให้อยู่ตรงกลาง ดังนั้นแร็กเกตของเราจึงถูกจัดชิดกับกึ่งกลางของเคอร์เซอร์

ตอนนี้เราสร้างแร็กเกตแล้ว เราต้องทำให้ลูกบอลกระดอนกับมัน

 ... int racketBounceRate = 20; ... void gameScreen() { ... watchRacketBounce(); ... } ... void watchRacketBounce() { float overhead = mouseY - pmouseY; if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { makeBounceBottom(mouseY); // racket moving up if (overhead<0) { ballY+=overhead; ballSpeedVert+=overhead; } } } }

และนี่คือผลลัพธ์:

ลูกบอลกระดอนบนแร็กเกตแต่มาหยุดเนื่องจากการเสียดสี

ดังนั้นสิ่งที่ watchRacketBounce() ทำคือทำให้แน่ใจว่าไม้และลูกบอลชนกัน มีสองสิ่งที่ต้องตรวจสอบที่นี่ ซึ่งก็คือถ้าลูกและแร็กเกตเรียงกันในแนวตั้งและแนวนอน อันดับแรก ถ้าคำสั่งตรวจสอบว่าพิกัด X ของด้านขวาของลูกบอลมากกว่าพิกัด X ของด้านซ้ายของแร็กเกตหรือไม่ (และในทางกลับกัน) ถ้าใช่ ข้อความที่สองตรวจสอบว่าระยะห่างระหว่างลูกบอลกับแร็กเกตน้อยกว่าหรือเท่ากับรัศมีของลูกบอลหรือไม่ (ซึ่งหมายความว่ากำลังชนกัน) ดังนั้นหากเงื่อนไขเหล่านี้ makeBounceBottom() จะถูกเรียกและลูกบอลจะกระดอนบนแร็กเกตของเรา (ที่ mouseY ที่แร็กเก็ตอยู่)

คุณสังเกตเห็น overhead ของตัวแปรที่คำนวณโดย mouseY - pmouseY หรือไม่? ตัวแปร pmouseX และ pmouseY เก็บพิกัดของเมาส์ไว้ที่เฟรมก่อนหน้า เนื่องจากเมาส์สามารถเคลื่อนที่ได้เร็วมาก จึงมีโอกาสสูงที่เราอาจตรวจไม่พบระยะห่างระหว่างลูกบอลกับแร็กเกตระหว่างเฟรมอย่างถูกต้อง หากเมาส์เคลื่อนเข้าหาลูกบอลเร็วพอ ดังนั้นเราจึงนำความแตกต่างของพิกัดของเมาส์ระหว่างเฟรมและพิจารณาด้วยในขณะตรวจจับระยะห่าง ยิ่งเมาส์เคลื่อนที่เร็วขึ้นเท่าใด ระยะห่างก็ยิ่งยอมรับได้

เราใช้ overhead ด้วยเหตุผลอื่น เราตรวจพบว่าเมาส์เคลื่อนที่ไปทางใดโดยการตรวจสอบเครื่องหมาย overhead หากค่าโสหุ้ยเป็นค่าลบ แสดงว่าเมาส์อยู่ด้านล่างของเฟรมก่อนหน้า ดังนั้นเมาส์ (แร็กเกต) ของเราจึงขยับขึ้น ในกรณีนั้น เราต้องการเพิ่มความเร็วพิเศษให้กับลูกบอลและเคลื่อนมันให้ไกลกว่าการกระดอนปกติเล็กน้อยเพื่อจำลองผลกระทบของการตีลูกบอลด้วยแร็กเกต หาก overhead น้อยกว่า 0 เราจะเพิ่มเข้าไปใน ballY และ ballSpeedVert เพื่อให้ลูกบอลสูงขึ้นและเร็วขึ้น ดังนั้นยิ่งแร็กเกตตีลูกบอลได้เร็วเท่าไหร่ แร็กเกตก็จะยิ่งสูงขึ้นและเร็วขึ้นเท่านั้น

ขั้นตอนการสอนการประมวลผล #4: การเคลื่อนที่ในแนวนอนและการควบคุมลูกบอล

ในส่วนนี้ เราจะเพิ่มการเคลื่อนไหวในแนวนอนให้กับลูกบอล จากนั้นเราจะทำให้สามารถควบคุมลูกบอลในแนวนอนด้วยแร็กเกตของเราได้ ไปเลย:

 ... // we will start with 0, but for we give 10 just for testing float ballSpeedHorizon = 10; ... void gameScreen() { ... applyHorizontalSpeed(); ... } ... void applyHorizontalSpeed(){ ballX += ballSpeedHorizon; ballSpeedHorizon -= (ballSpeedHorizon * airfriction); } void makeBounceLeft(float surface){ ballX = surface+(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } void makeBounceRight(float surface){ ballX = surface-(ballSize/2); ballSpeedHorizon*=-1; ballSpeedHorizon -= (ballSpeedHorizon * friction); } ... void keepInScreen() { ... if (ballX-(ballSize/2) < 0){ makeBounceLeft(0); } if (ballX+(ballSize/2) > width){ makeBounceRight(width); } }

และผลลัพธ์ก็คือ:

ตอนนี้ลูกบอลกระเด้งในแนวนอนเช่นกัน

แนวคิดในที่นี้ก็เหมือนกับที่เราทำในการเคลื่อนที่ในแนวดิ่ง เราสร้างตัวแปรความเร็วแนวนอน ballSpeedHorizon เราสร้างวิธีการใช้ความเร็วแนวนอนกับ ballX และขจัดแรงเสียดทานของอากาศ เราได้เพิ่มคำสั่ง if อีกสองคำสั่งในวิธี keepInScreen() ซึ่งจะคอยดูลูกบอลจากการชนขอบด้านซ้ายและขวาของหน้าจอ ในที่สุด เราก็สร้าง makeBounceLeft() และ makeBounceRight() เพื่อจัดการกับการเด้งจากซ้ายและขวา

ตอนนี้เราได้เพิ่มความเร็วแนวนอนให้กับเกมแล้ว เราต้องการควบคุมลูกบอลด้วยแร็กเกต เช่นเดียวกับเกม Breakout Atari ที่มีชื่อเสียงและในเกมทำลายอิฐอื่น ๆ ทั้งหมด ลูกบอลควรไปทางซ้ายหรือขวาตามจุดบนแร็กเกตที่ตี ขอบของไม้แร็กเกตควรให้ความเร็วลูกในแนวราบมากขึ้น ในขณะที่ไม้ตรงกลางไม่ควรมีผลใดๆ รหัสก่อน:

 void watchRacketBounce() { ... if ((ballX+(ballSize/2) > mouseX-(racketWidth/2)) && (ballX-(ballSize/2) < mouseX+(racketWidth/2))) { if (dist(ballX, ballY, ballX, mouseY)<=(ballSize/2)+abs(overhead)) { ... ballSpeedHorizon = (ballX - mouseX)/5; ... } } }

ผลลัพธ์คือ:

ฟิสิกส์แนวนอนสไตล์ฝ่าวงล้อม

การเพิ่มบรรทัดง่าย ๆ นั้นลงใน watchRacketBounce() ก็ทำได้สำเร็จ สิ่งที่เราทำคือเรากำหนดระยะห่างของจุดที่ลูกบอลกระทบจากจุดศูนย์กลางของแร็กเกตด้วย ballX - mouseX . จากนั้นเราก็ทำให้มันเป็นความเร็วแนวนอน ความแตกต่างที่แท้จริงนั้นมากเกินไป ดังนั้นฉันจึงลองสองสามครั้งและพบว่าหนึ่งในสิบของค่าให้ความรู้สึกเป็นธรรมชาติที่สุด

ขั้นตอนการสอนการประมวลผล #5: การสร้างกำแพง

สเก็ตช์ของเราเริ่มดูเหมือนเกมมากขึ้นในแต่ละขั้นตอน ในขั้นตอนนี้ เราจะเพิ่มกำแพงเคลื่อนที่ไปทางซ้าย เช่นเดียวกับ Flappy Bird:

 ... int wallSpeed = 5; int wallInterval = 1000; float lastAddTime = 0; int minGapHeight = 200; int maxGapHeight = 300; int wallWidth = 80; color wallColors = color(0); // This arraylist stores data of the gaps between the walls. Actuals walls are drawn accordingly. // [gapWallX, gapWallY, gapWallWidth, gapWallHeight] ArrayList<int[]> walls = new ArrayList<int[]>(); ... void gameScreen() { ... wallAdder(); wallHandler(); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { int randHeight = round(random(minGapHeight, maxGapHeight)); int randY = round(random(0, height-randHeight)); // {gapWallX, gapWallY, gapWallWidth, gapWallHeight} int[] randWall = {width, randY, wallWidth, randHeight}; walls.add(randWall); lastAddTime = millis(); } } void wallHandler() { for (int i = 0; i < walls.size(); i++) { wallRemover(i); wallMover(i); wallDrawer(i); } } void wallDrawer(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; // draw actual walls rectMode(CORNER); fill(wallColors); rect(gapWallX, 0, gapWallWidth, gapWallY); rect(gapWallX, gapWallY+gapWallHeight, gapWallWidth, height-(gapWallY+gapWallHeight)); } void wallMover(int index) { int[] wall = walls.get(index); wall[0] -= wallSpeed; } void wallRemover(int index) { int[] wall = walls.get(index); if (wall[0]+wall[2] <= 0) { walls.remove(index); } }

และสิ่งนี้ส่งผลให้:

ลูกบอลกระดอนผ่านระดับที่มีกำแพง

แม้ว่าโค้ดจะดูยาวและข่มขู่ แต่ฉันสัญญาว่าไม่มีอะไรเข้าใจยาก สิ่งแรกที่ควรสังเกตคือ ArrayList สำหรับผู้ที่ไม่รู้ว่า ArrayList คืออะไร มันเป็นเพียงการใช้งานรายการที่ทำหน้าที่เหมือน Array แต่มีข้อดีเหนือกว่าบางอย่าง มันสามารถปรับขนาดได้ มันมีวิธีการที่มีประโยชน์เช่น list.add(index) , list.get(index) และ list.remove(index) เราเก็บข้อมูลวอลล์เป็นอาร์เรย์จำนวนเต็มภายในรายการอาร์เรย์ ข้อมูลที่เราเก็บไว้ในอาร์เรย์นั้นมีไว้สำหรับช่องว่างระหว่างสองกำแพง อาร์เรย์มีค่าต่อไปนี้:

 [gap wall X, gap wall Y, gap wall width, gap wall height]

ผนังจริงถูกวาดตามค่าผนังช่องว่าง โปรดทราบว่าสิ่งเหล่านี้สามารถจัดการได้ดีกว่าและสะอาดกว่าโดยใช้คลาส แต่เนื่องจากการใช้ Object Oriented Programming (OOP) ไม่อยู่ในขอบเขตของบทช่วยสอนการประมวลผลนี้ เราจะจัดการกับมันอย่างไร เรามีสองวิธีพื้นฐานในการจัดการ wall, wallAdder() และ wallHandler

wallAdder() จะเพิ่มวอลล์ใหม่ในทุกๆ มิลลิวินาทีของ wallInterval ให้กับรายการอาร์เรย์ เรามีตัวแปร lastAddTime ซึ่งเก็บเวลาที่เพิ่มกำแพงสุดท้าย (เป็นมิลลิวินาที) หากมิลลิวินาทีปัจจุบัน millis() ลบด้วย lastAddTime มิลลิวินาทีที่เพิ่มล่าสุด นั้นมากกว่าค่าช่วงเวลาของเรา wallInterval แสดงว่าถึงเวลาแล้วที่จะต้องเพิ่มวอลล์ใหม่ จากนั้นตัวแปรช่องว่างสุ่มจะถูกสร้างขึ้นตามตัวแปรส่วนกลางที่กำหนดไว้ที่ด้านบนสุด จากนั้นวอลล์ใหม่ (อาร์เรย์จำนวนเต็มที่เก็บข้อมูลช่องว่างวอลล์) จะถูกเพิ่มลงในรายการอาร์เรย์ และ lastAddTime ถูกตั้งค่าเป็นมิลลิวินาทีปัจจุบัน millis()

wallHandler() วนรอบกำแพงปัจจุบันที่อยู่ในรายการอาร์เรย์ และสำหรับแต่ละรายการในแต่ละลูป จะเรียก wallRemover(i) , wallMover(i) และ wallDrawer(i) ด้วยค่าดัชนีของรายการอาร์เรย์ วิธีการเหล่านี้ทำตามชื่อของพวกเขา wallDrawer() ดึงผนังจริงตามข้อมูลผนังช่องว่าง มันคว้าอาร์เรย์ข้อมูลวอลล์จาก arraylist และเรียกเมธอด rect() เพื่อดึงวอลล์ไปยังตำแหน่งที่ควรจะเป็น wallMover() ดึงองค์ประกอบจากรายการอาร์เรย์ เปลี่ยนตำแหน่ง X ตามตัวแปรทั่วโลกของ wallSpeed สุดท้าย wallRemover() จะลบกำแพงออกจากรายการอาร์เรย์ที่อยู่นอกหน้าจอ ถ้าเราไม่ทำเช่นนั้น การประมวลผลจะถือว่าพวกเขายังคงอยู่ในหน้าจอ และนั่นจะเป็นการสูญเสียประสิทธิภาพอย่างมาก ดังนั้นเมื่อกำแพงถูกลบออกจากรายการอาร์เรย์ ผนังจะไม่ถูกวาดในลูปที่ตามมา

สิ่งท้าทายสุดท้ายที่เหลือที่ต้องทำคือการตรวจจับการชนระหว่างลูกบอลกับกำแพง

 void wallHandler() { for (int i = 0; i < walls.size(); i++) { ... watchWallCollision(i); } } ... void watchWallCollision(int index) { int[] wall = walls.get(index); // get gap wall settings int gapWallX = wall[0]; int gapWallY = wall[1]; int gapWallWidth = wall[2]; int gapWallHeight = wall[3]; int wallTopX = gapWallX; int wallTopY = 0; int wallTopWidth = gapWallWidth; int wallTopHeight = gapWallY; int wallBottomX = gapWallX; int wallBottomY = gapWallY+gapWallHeight; int wallBottomWidth = gapWallWidth; int wallBottomHeight = height-(gapWallY+gapWallHeight); if ( (ballX+(ballSize/2)>wallTopX) && (ballX-(ballSize/2)<wallTopX+wallTopWidth) && (ballY+(ballSize/2)>wallTopY) && (ballY-(ballSize/2)<wallTopY+wallTopHeight) ) { // collides with upper wall } if ( (ballX+(ballSize/2)>wallBottomX) && (ballX-(ballSize/2)<wallBottomX+wallBottomWidth) && (ballY+(ballSize/2)>wallBottomY) && (ballY-(ballSize/2)<wallBottomY+wallBottomHeight) ) { // collides with lower wall } }

watchWallCollision() ถูกเรียกสำหรับแต่ละกำแพงในแต่ละลูป เราจับพิกัดของผนังช่องว่าง คำนวณพิกัดของผนังจริง (บนและล่าง) และตรวจสอบว่าพิกัดของลูกบอลชนกับผนังหรือไม่

ขั้นตอนการสอนการประมวลผล #6: สุขภาพและคะแนน

ตอนนี้เราสามารถตรวจจับการชนของลูกบอลและกำแพงได้แล้ว เราจึงตัดสินใจเกี่ยวกับกลไกของเกมได้ หลังจากปรับแต่งเกมแล้ว ฉันก็สามารถทำให้เกมเล่นได้ค่อนข้างดี แต่ถึงกระนั้นมันก็ยากมาก ความคิดแรกของฉันเกี่ยวกับเกมนี้คือการทำให้มันเหมือน Flappy Bird เมื่อลูกบอลสัมผัสกับกำแพง เกมจะจบลง แต่แล้วฉันก็รู้ว่ามันเป็นไปไม่ได้ที่จะเล่น นี่คือสิ่งที่ฉันคิดว่า:

ควรมีแถบสุขภาพที่ด้านบนของลูก ลูกบอลควรเสียสุขภาพในขณะที่สัมผัสกับผนัง ด้วยตรรกะนี้ มันไม่สมเหตุสมผลเลยที่จะทำให้ลูกบอลเด้งกลับจากกำแพง ดังนั้นเมื่อสุขภาพเป็น 0 เกมจะจบลงและเราควรสลับไปที่เกมผ่านหน้าจอ ไปเลย:

 int maxHealth = 100; float health = 100; float healthDecrease = 1; int healthBarWidth = 60; ... void gameScreen() { ... drawHealthBar(); ... } ... void drawHealthBar() { // Make it borderless: noStroke(); fill(236, 240, 241); rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth, 5); if (health > 60) { fill(46, 204, 113); } else if (health > 30) { fill(230, 126, 34); } else { fill(231, 76, 60); } rectMode(CORNER); rect(ballX-(healthBarWidth/2), ballY - 30, healthBarWidth*(health/maxHealth), 5); } void decreaseHealth(){ health -= healthDecrease; if (health <= 0){ gameOver(); } }

และนี่คือการดำเนินการง่ายๆ:

ลูกบอลที่มีแถบสุขภาพกระเด้งผ่านระดับ สูญเสียสุขภาพเมื่อชนกับกำแพง

เราสร้าง health ตัวแปรระดับโลกเพื่อรักษาสุขภาพของลูกบอล จากนั้นจึงสร้างเมธอด drawHealthBar() ซึ่งวาดสี่เหลี่ยมสองรูปบนลูกบอล อันแรกคือแถบสุขภาพพื้นฐาน อีกอันหนึ่งคือแถบที่ใช้งานซึ่งแสดงสุขภาพปัจจุบัน ความกว้างของส่วนที่สองเป็นไดนามิก และคำนวณด้วย healthBarWidth*(health/maxHealth) อัตราส่วนของสุขภาพปัจจุบันของเราเทียบกับความกว้างของแถบสุขภาพ สุดท้ายสีเติมจะถูกกำหนดตามค่าของสุขภาพ สุดท้ายแต่ไม่ท้ายสุด คะแนน :

 ... void gameOverScreen() { background(0); textAlign(CENTER); fill(255); textSize(30); text("Game Over", height/2, width/2 - 20); textSize(15); text("Click to Restart", height/2, width/2 + 10); } ... void wallAdder() { if (millis()-lastAddTime > wallInterval) { ... // added another value at the end of the array int[] randWall = {width, randY, wallWidth, randHeight, 0}; ... } } void watchWallCollision(int index) { ... int wallScored = wall[4]; ... if (ballX > gapWallX+(gapWallWidth/2) && wallScored==0) { wallScored=1; wall[4]=1; score(); } } void score() { score++; } void printScore(){ textAlign(CENTER); fill(0); textSize(30); text(score, height/2, 50); }

เราต้องยิงประตูเมื่อบอลผ่านกำแพง แต่เราต้องเพิ่มคะแนนสูงสุด 1 คะแนนต่อกำแพง หมายความว่า ถ้าบอลผ่านกำแพงมากกว่าย้อนและส่งอีก ไม่ควรบวกคะแนนอื่น เพื่อให้บรรลุสิ่งนี้ เราได้เพิ่มตัวแปรอื่นให้กับ gap wall array ภายใน arraylist ตัวแปรใหม่เก็บ 0 ถ้าลูกบอลยังไม่ผ่านกำแพงนั้นและ 1 ถ้าผ่านกำแพงนั้น จากนั้น เราแก้ไข watchWallCollision() เราได้เพิ่มเงื่อนไขที่ยิงวิธี score() และทำเครื่องหมายว่ากำแพงว่าผ่านเมื่อลูกบอลผ่านกำแพงที่ไม่เคยผ่านมาก่อน

ตอนนี้เราใกล้ถึงจุดสิ้นสุดแล้ว สิ่งสุดท้ายที่ต้องทำคือใช้การ click to restart เกมใหม่ผ่านหน้าจอ เราจำเป็นต้องตั้งค่าตัวแปรทั้งหมดที่เราใช้เป็นค่าเริ่มต้นและเริ่มเกมใหม่ นี่คือ:

 ... public void mousePressed() { ... if (gameScreen==2){ restart(); } } ... void restart() { score = 0; health = maxHealth; ballX=width/4; ballY=height/5; lastAddTime = 0; walls.clear(); gameScreen = 0; }

มาเพิ่มสีสันกันดีกว่า

เครื่องปัดพงษ์ที่สมบูรณ์ในสีเต็มรูปแบบ

โว้ว! เรามี Flappy Pong!

รหัสเกมการประมวลผลแบบเต็มสามารถพบได้ที่นี่

การโอนรหัสเกมการประมวลผลไปยังเว็บโดยใช้ p5.js

p5.js เป็นไลบรารี JavaScript ที่มีรูปแบบคล้ายกับภาษาโปรแกรมการประมวลผล ไม่ใช่ห้องสมุดที่สามารถเรียกใช้รหัสการประมวลผลที่มีอยู่ได้ แทน p5.js ต้องเขียนโค้ด JavaScript จริง—คล้ายกับพอร์ต JavaScript ของการประมวลผลที่เรียกว่า Processing.js งานของเราคือการแปลงรหัสการประมวลผลเป็น JavaScript โดยใช้ p5.js API ไลบรารีมีชุดของฟังก์ชันและไวยากรณ์ที่คล้ายกับการประมวลผล และเราต้องทำการเปลี่ยนแปลงบางอย่างในโค้ดของเราเพื่อให้ทำงานใน JavaScript แต่เนื่องจากทั้งการประมวลผลและ JavaScript มีความคล้ายคลึงกันกับ Java จึงมีการก้าวกระโดดน้อยกว่าที่คิด . แม้ว่าคุณจะไม่ใช่นักพัฒนา JavaScript การเปลี่ยนแปลงนั้นก็ไม่สำคัญ และคุณควรติดตามได้ดี

ก่อนอื่น เราต้องสร้าง index.html อย่างง่าย และเพิ่ม p5.min.js ในส่วนหัวของเรา เราต้องสร้างไฟล์อื่นที่เรียกว่า flappy_pong.js ซึ่งจะเป็นที่เก็บโค้ดที่แปลงแล้วของเรา

 <html> <head> <title>Flappy Pong</title> <script tyle="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.4.19/p5.min.js"></script> <script tyle="text/javascript" src="flappy_pong.js"></script> <style> canvas { box-shadow: 0 0 20px lightgray; } </style> </head> <body> </body> </html>

กลยุทธ์ของเราในขณะแปลงโค้ดควรคัดลอกและวางโค้ดทั้งหมดของเราลงใน flappy_pong.js แล้วทำการเปลี่ยนแปลงทั้งหมด และนั่นคือสิ่งที่ฉันทำ และนี่คือขั้นตอนที่ฉันใช้เพื่ออัปเดตโค้ด:

  • Javascript เป็นภาษาที่ไม่ได้พิมพ์ (ไม่มีการประกาศประเภทเช่น int และ float ) ดังนั้นเราต้องเปลี่ยนการประกาศประเภททั้งหมดเป็น var

  • ไม่มี void ใน Javascript เราควรเปลี่ยนการ function ทั้งหมด

  • เราจำเป็นต้องลบการประกาศประเภทของอาร์กิวเมนต์ออกจากฟังก์ชันลายเซ็น (เช่น void wallMover(var index) { เพื่อ function wallMover(index) { )

  • ไม่มี ArrayList ใน JavaScript แต่เราสามารถบรรลุสิ่งเดียวกันได้โดยใช้อาร์เรย์ JavaScript เราทำการเปลี่ยนแปลงดังต่อไปนี้:

    • ArrayList<int[]> walls = new ArrayList<int[]>(); ถึง var walls = [];
    • walls.clear(); กับ walls = [];
    • walls.add(randWall); ไปที่ walls.push(randWall);
    • walls.remove(index); กับ walls.splice(index,1);
    • walls.get(index); กับ walls[index]
    • walls.size() กับ walls.length
  • เปลี่ยนการประกาศอาร์เรย์ var randWall = {width, randY, wallWidth, randHeight, 0}; ถึง var randWall = [width, randY, wallWidth, randHeight, 0];

  • ลบคำหลัก public ทั้งหมด

  • ย้ายการประกาศ color(0) ทั้งหมดไปยัง function setup() เนื่องจากจะไม่มีการกำหนด color() ก่อนการเรียก setup()

  • เปลี่ยน size(500, 500); เพื่อ createCanvas(500, 500);

  • เปลี่ยนชื่อ function gameScreen(){ เป็นอย่างอื่นเช่น function gamePlayScreen(){ เพราะเรามีตัวแปรส่วนกลางชื่อ gameScreen แล้ว เมื่อเราทำงานกับการประมวลผล ตัวหนึ่งเป็นฟังก์ชัน และอีกตัวเป็นตัวแปร int แต่ JavaScript ทำให้สิ่งเหล่านี้สับสนเนื่องจากไม่ได้พิมพ์

  • เช่นเดียวกันสำหรับ score() ฉันเปลี่ยนชื่อเป็น addScore()

คุณสามารถดูโค้ด JavaScript แบบเต็มที่ครอบคลุมทุกอย่างในบทช่วยสอนการประมวลผลนี้ได้ที่นี่

กำลังประมวลผลรหัสเกม: คุณเองก็ทำได้

ในบทช่วยสอนการประมวลผลนี้ ฉันพยายามอธิบายวิธีสร้างเกมที่ง่ายมาก อย่างไรก็ตาม สิ่งที่เราทำในบทความนี้เป็นเพียงส่วนเล็กๆ ของภูเขาน้ำแข็ง ด้วยภาษาโปรแกรมการประมวลผล คุณสามารถทำอะไรก็ได้ ในความคิดของฉัน มันเป็นเครื่องมือที่ดีที่สุดในการตั้งโปรแกรมสิ่งที่คุณกำลังจินตนาการ ความตั้งใจจริงของฉันกับบทช่วยสอนการประมวลผลนี้ไม่ใช่การสอนการประมวลผลและการสร้างเกม เพื่อพิสูจน์ว่าการเขียนโปรแกรมไม่ได้ยากขนาดนั้น การสร้างเกมของคุณเองไม่ใช่แค่ความฝัน ฉันต้องการแสดงให้คุณเห็นว่าคุณทำได้โดยใช้ความพยายามและความกระตือรือร้นเพียงเล็กน้อย ฉันหวังว่าบทความทั้งสองนี้จะสร้างแรงบันดาลใจให้ทุกคนได้ลองเขียนโปรแกรมดู