ברוכים הבאים לשיעור שישי בקורס תכנות משחקים בחינם. בשיעור הזה נבצע את הלוגיקה של סיבוב המטבע שיצרנו בעזרת threejs עם צורה גיאומטרית של צילינדר בעזרת CylinderGeometry . כפי שציינתי אנחנו עשה טריק קטן על מנת לקבל את האפקט הזה מכוון שלא הכי פשוט לצבוע בטקסטרה אחרת את צדדי הצילינדר ויש טכניקות יותר מורכבות לעשות זאת אבל גם טריק נחמד זה לקורס חינמי זה יהיה לכם לערך רב בהבנה הכללית בעולם ה webGL . אז נחזור על השיעורים הקודמים כהרגלנו בקודש:
להלן רצף השיעורים בקורס תכנות משחקים – webgl
- קורס תכנות משחקים – webgl – שיעור ראשון – הקמת הפרויקט
- קורס תכנות משחקים – webgl – שיעור שני- הסצנה והמצלמה
- קורס תכנות משחקים – webgl – שיעור שלישי- ציור של אובייקט המטבע
- קורס תכנות משחקים – webgl – שיעור רביעי- תנועה של האובייקט הגרפי במרחב
- קורס תכנות משחקים – webgl – שיעור חמישי – עיצוב המטבע ושימוש בתאורה
מימוש לוגיקת סיבוב המטבע במשחק tossup
לפני שניגש לקוד חשוב להבין קודם מה רוצים להשיג , כלומר לכתוב על דף את התנאים שיש למשחק שלנו, למשך:
- התחל את המשחק
- זריקה רנדומאלית – כלומר מספר סיבובים רנדומלי
- מיהו הצד הזוכה – הצד המופיע כלפי מעלה.
- המטבע נזרק כלפי מעלה ולכן נראה להתייחס לדרך שהוא עושה כלפי מעלה וכלפי מטה.
עוד אנחנו נרצה לעשות שינויים בUI ולכן הוספתי כבר את הקוד של היו איי אז נעבור עליו גם בהמשך.
נתחיל להבין את הדברים תוך כדי הסתכלות על הקוד:
import React, { Component } from 'react'; import * as THREE from 'three'; import side1 from './side1.png'; import side2 from './side2.png'; class Coin extends Component { constructor(props) { super(props); this.state = { textureSide1: new THREE.TextureLoader().load(side1), textureSide2: new THREE.TextureLoader().load(side2) }; this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 1000); this.scene = new THREE.Scene(); this.renderer = null; this.cylinder = null; this.canvasRef = React.createRef(); } initialObj() { this.camera.position.z = 200; this.renderer = new THREE.WebGLRenderer({ canvas: this.canvasRef.current, alpha: true }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setSize(window.innerWidth, window.innerHeight); } componentDidMount() { this.initialObj(); this.addLights(); this.createShape(); } componentDidUpdate(){ if(this.props.play) this.animate(); } createShape() { const geometry = new THREE.CylinderGeometry(10, 10, 1, 100); const material = new THREE.MeshStandardMaterial({ map: this.state.textureSide1, color: 0xFFD700, metalness: 0.3, roughness: 0.3 }) this.cylinder = new THREE.Mesh(geometry, material); this.cylinder.rotation.x = Math.PI / 2; this.cylinder.rotation.y = Math.PI / 2; this.scene.add(this.cylinder); } addLights() { this.scene.add(new THREE.AmbientLight(0xffffff)) // Left point light const pointLightLeft = new THREE.PointLight(0xff4422, 1) pointLightLeft.position.set(-20,-10,10) this.scene.add(pointLightLeft) // Right point light const pointLightRight = new THREE.PointLight(0x44ff88, 1) pointLightRight.position.set(20,10,10) this.scene.add(pointLightRight) // Top point light const pointLightTop = new THREE.PointLight(0xdd3311, 1) pointLightTop.position.set(0,3,2) this.scene.add(pointLightTop); } animate() { let frames = 0; let limit = this.props.rand; let spinsCount = 0; const spininngCoin = setInterval(() => { frames++; if (frames % limit === 0) { clearInterval(spininngCoin); this.props.setGame(false); if (spinsCount % 2) { this.cylinder.rotation.x = Math.PI / 2 + Math.PI; this.props.result('bitcoin'); } else { this.cylinder.rotation.x = Math.PI / 2; this.props.result('codcodile'); } } if (frames < limit / 2) { this.camera.position.z -= 0.1; } else { this.camera.position.z += 0.1; } spinsCount = ~~(this.cylinder.rotation.x / Math.PI); if (spinsCount % 2) { this.cylinder.material.map = this.state.textureSide1; } else { this.cylinder.material.map = this.state.textureSide2; } this.cylinder.rotation.x += 0.025 * Math.PI; this.renderer.render(this.scene, this.camera); }); } render() { return ( <canvas ref={this.canvasRef} className="canvasClass"></canvas> ) } } export default Coin;
אז הוספנו לא מעט קוד! לרוב הלוגיקה תדרוש קצת עבודת כתיבה. נתחיל מההתחלה של קובץ coin . קודם כל נסתכל על props שעד עכשיו לא עשינו בו שימוש , ונעשה בו בהמשך, אז רק לתשומת לבכם. נכון לעכשיו המטבע לא יופיע כלל על המסך כי יש להתחיל את המשחק. שימו לב בשורה 37 שבודקת אם המשחק התחיל או לא.
והנה החלק המעניין שנמצא בפונקציית animate, אפשר לראות שהגדרנו כמה דברים נוספים וגם הרבה שינויים בתוך האינטרבל שלנו. אז יש לנו משתנה שסופר את ה frames יש את limit שנותן לנו מספר רנדומלי שהגדרתי אותו בין 1000 – 2000 והוא ישתנה בכל זריקה דרך ה props. ברגע שמספר ה frames יהיה זהה ללימיט נסיים את תהליך של האינטרבל ונציג בהתאם את התוצאה שיצאה בהתאם ל spinCount שסופר את מספר סיבובי המטבע ביחס של פאי כלומר סופר חצאי סיבובים (סיבוב שלם זה 2 פאי). כמו כן בשורה 95 יש לנו תנאי שיודע לשנות את מיקום המטבע ביחס לציר ה Z וכן נקבל את התחושה שהמטבע עולה ויורד. בשורה 101 יש תנאי שיודע לשנות את הטקסטורה של הצילינדר וכך מקבלים את המראה שהמטבע מסתובב ורואים את שני צדדיו אם כי זה טריק כי אין שני צדדים, יש טקסטורה אחת על כל הצילינדר ואנחנו משנים אותה כל פאי סיבובים (כלומר חצי סיבוב) . ובהמשך בשורה 106 נקבע את מהירות הסיבוב. אם תשחקו עם המספר 0.025 תוכלו לזרז או להאט את מהירות הסיבוב.
import React, {useState} from "react"; import Coin from "./coin"; import './App.css'; import useRandom from './useRandom' function App() { const [isPlay, playGame] = useState(false); const [result, setResult] = useState(''); const {randNum} = useRandom(0); return ( <div className="App"> <Coin play={isPlay} setGame={playGame} result={setResult} rand={randNum} /> <div className={"controls-container " + (isPlay ? "bottom-pos" : null)}> {!isPlay && <div className="content-for-game"> {/* {!result && <img src={side} alt="coin image" />} */} <h1>{result ? 'You Have Got : ' + result : 'Play Toss Up Now!'}</h1> </div>} <button className="button-77" disabled={isPlay} onClick={()=>playGame(!isPlay)}>Toss Up!</button> </div> </div> ); } export default App;
להלן הקוד של רכיב הapp , רכיב מסוג functional component , שמעביר מספר משתנים באמצעות הprops. אפשר לראות גם שיצרתי custom hook, ללא איזה צורך מיוחד רציתי להפריד את הלוגיקה הזאתי אבל בתכלס אפשר גם לעשות את זה ברכיב עצמו.. לפרקטיקה שלנו זה נחמד להשתמש בhooks עם react.
להלן custom hook:
function useRandom() { const randNum = Math.floor(Math.random() * (1000 - 500) + 500); return {randNum}; } export default useRandom;
והנה קצת css שיהיה נחמד בעין .
.App{ width: 100%; height: 100%; background-image: url('./deck.jpg'); background-repeat: no-repeat; background-size: cover; } .canvasClass { width: 100%; height: 100%; background-color: #00000080; } .controls-container { text-align: center; position: absolute; top: 70%; left: 50%; transform: translate(-50% , -50%); } .controls-container h1 { font-size: 40px; font-weight: bold; color: #fff; } /* CSS */ .button-77 { align-items: center; appearance: none; background-clip: padding-box; background-color: initial; background-image: none; border-style: none; box-sizing: border-box; color: #fff; cursor: pointer; display: inline-block; flex-direction: row; flex-shrink: 0; font-family: Eina01,sans-serif; font-size: 16px; font-weight: 800; justify-content: center; line-height: 24px; margin: 0; min-height: 64px; outline: none; overflow: visible; padding: 19px 26px; pointer-events: auto; position: relative; text-align: center; text-decoration: none; text-transform: none; user-select: none; -webkit-user-select: none; touch-action: manipulation; vertical-align: middle; width: auto; word-break: keep-all; z-index: 0; } @media (min-width: 768px) { .button-77 { padding: 19px 32px; } } .button-77:before, .button-77:after { border-radius: 80px; } .button-77:before { background-color: rgba(249, 58, 19, .32); content: ""; display: block; height: 100%; left: 0; overflow: hidden; position: absolute; top: 0; width: 100%; z-index: -2; } .button-77:after { background-color: initial; background-image: linear-gradient(92.83deg, #ff7426 0, #f93a13 100%); bottom: 4px; content: ""; display: block; left: 4px; overflow: hidden; position: absolute; right: 4px; top: 4px; transition: all 100ms ease-out; z-index: -1; } .button-77:hover:not(:disabled):after { bottom: 0; left: 0; right: 0; top: 0; transition-timing-function: ease-in; } .button-77:active:not(:disabled) { color: #ccc; } .button-77:active:not(:disabled):after { background-image: linear-gradient(0deg, rgba(0, 0, 0, .2), rgba(0, 0, 0, .2)), linear-gradient(92.83deg, #ff7426 0, #f93a13 100%); bottom: 4px; left: 4px; right: 4px; top: 4px; } .button-77:disabled { cursor: default; opacity: .24; }
סיכום שיעור חמישי webgl | תכנות משחקים
באופן כללי סיימנו את המשחק ואם יכולים לשחק במשחק הכי בסיסי בעולם של הטלת מטבע. אם כי יש עוד איזה משהו שבטח חשבתם עליו שעוד לא מימשנו וזה מהירות המטבע כשהוא עולה ויורד , כרגע המטבע נע באופן לינארי אבל אנחנו יודעים שבמציאות יש את נושא הגרביטציה , ולכן מה שנעשה בשיעור הבא זה נוסיף כוח G שיופעל על המטבע שלנו כשהוא קופץ לאוויר . הסיבה שהפרדתי את הנושא לפוסט נפרד הוא כי נושא הגרביטציה הוא בסיסי שחוזר על עצמו המון וחשבתי שפוסט נפרד יהיה מכובד יותר, אז נתראה בפוסט האחרון של קורס תכנות משחקים webgl.