ตัวอย่างการสร้างเกม Rock Paper Scissors หรือ เป่ายิ๊งฉุบ ด้วย LEAP Motion กับ Javascript กับงานที่ไป Contribute Repo ร่วมกับนักพัฒนาคนอื่น
ในเมื่อเริ่ม Join งานนวัตกรรมแล้ว โจทย์หนึ่งคือ Leap Motion กับการเปลี่ยนการเรียนการสอนด้านการพัฒนา สื่อเชิงโต้ตอบ ก็เลยต้องไป Join Developer Group มากมาย และก่อนหน้านี้พักใหญ่ๆ ก็ได้ไป Contribute ตัว Repo ช่วย nandico (https://github.com/nandico) ในเรื่องของการแก้ Javascript ช่วยมาประมาณหนึ่งจนเค้าเงียบหายไปก็เลยขอ อนุญาตินำตัว Repo ที่เราไปช่วยมาเขียน tutorial ซะเลย ซึ่งเจ้าตัวก็อนุญาติ (เดี๋ยวนี้ทำงานสายวิชาการต้องมีการให้เครดิตผู้พัฒนาเริ่มต้นนะครับ)
งานนี้ก็เช่นเดิมครับ เอามาปรับแต่งเขียนใหม่เล็กน้อย บางไฟล์ก็เป็นของ nandico ไปเลยคือ LeapHelper.js ไม่ต้องไปแก้อะไร เสียเวลา
สร้าง File HTML ขึ้นมาก่อนครับใน XAMPP หรือ Apache ก็ได้
<html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width"> <title>Javascript Rock Paper Scissors LEAP MOTION</title> <link rel="stylesheet" type="text/css" href="main.css"> <script type="text/javascript" src="LeapHelper.js"></script> <!--Function Here--> </head> <body onload="init();"> <div align="center"> <div id="connection" style="display:block; color:10px;"><span style="color:red;">Device Not Connect</span></div> <div id="game" style="visibility:hidden"> <div id="output"></div> </div> </div> </body> </html>
โดยการเตรียมพร้อมคือการ เตรียมสร้าง CONSTANT ที่เกี่ยวข้องกับ State ของมือ และการรับค่า Detect ตัว Leap Motion ของเราครับ
ตามด้วย Style Sheet ของเราเล็กน้อย
@charset "UTF-8"; body { background-color: #FFF; font-family: 'Tahoma', sans-serif; color:#R3R3R3; } #output { font-family: 'Tahoma', sans-serif; color:#0000; font-size:19px; }
ตามด้วย Function ของเราที่ต้องสร้างขึ้นมาครับ ให้สร้างไฟล์ชื่อ function.js ครับ
function AllHandsSign() { this.lh = new LeapHelper(); // simple helper to count pointables and related functions this.mode = -1; // app mode in state machine this.loopTID = null; // reference to timer handling main loop function this.count = 3; // stores the count-down to start the game this.countTID = null; // reference to timer handling the countdown this.answerTID = null; // reference to timer handling the answer window this.nextRoundTID = null; // reference to timer waiting next round this.playerScore = 0; // store player score this.cpuScore = 0; // store cpu score this.dumbuserHand = null; // if the user put the hand over the sensor before counting computer 'sees' it and won this.cpuChoice = -1; // store cpu hand in the turn this.userChoice = -1; // store user hand in the turn this.userLosetime = false; // used when users take too long time to put a hand in a turn. User will lose the turn this.handHistory = new Array(); // store detected hands in loop to ensure correct detection /** * @desc System init */ this.init = function() { this.loopTID = window.setInterval( "rps.loop()", 10 ); this.setMode( AllHandsSign.MODE_READY ); } this.loop = function() { // transfer management of main loop to specific loop routines switch( this.mode ) { case AllHandsSign.MODE_READY: this.loopReady(); break; case AllHandsSign.MODE_321: this.loop321(); break; case AllHandsSign.MODE_LISTEN: this.loopListen(); break; } } this.loopReady = function() { hand = this.lh.getMoreProeminentHand(); if( hand ) { if( rps.checkHand( hand ) == AllHandsSign.SCISSORS ) { this.countToPlay(); } } } this.loop321 = function() { // detects if user put the hand before the right moment hand = this.lh.getMoreProeminentHand(); if( hand ) { this.dumbuserHand = this.checkHand( hand ); } } /** * @desc system still in this state while waiting for user * hand. Will move forward when finds a hand or when user * looses */ this.loopListen = function() { if( this.userLosetime ) return; hand = this.lh.getMoreProeminentHand(); if( hand ) { this.handHistory.push( this.checkHand( hand ) ); } var handCheck = new Array(); for( var i = this.handHistory.length - 1; i >= 0 && i > this.handHistory.length - ( AllHandsSign.MOVEMENT_CATCH_COUNT + 1 ); i -- ) { handCheck.push( this.handHistory[ i ] ); } repeatCount = 0; // MOVEMENT_CATCH_COUNT says to system how many frames in the past must be confirmed // in a same position to take this as a valid hand if( handCheck.length == AllHandsSign.MOVEMENT_CATCH_COUNT ) { for( i = 1; i < handCheck.length; i++ ) { if( handCheck[ i ] == handCheck[ 0 ] ) repeatCount ++; } } if( repeatCount == ( AllHandsSign.MOVEMENT_CATCH_COUNT - 1 ) ) { // now we got an confirmed user hand this.listenCatchedHand( handCheck[ 0 ] ); } } /** * @desc system mode setter */ this.setMode = function( mode ) { switch( mode ) { case AllHandsSign.MODE_READY: this.setModeReady(); break; case AllHandsSign.MODE_321: this.countToPlay(); break; case AllHandsSign.MODE_LISTEN: this.setModeListen(); break; case AllHandsSign.MODE_EVALUATE: this.setModeEvaluate(); break; } } /** * @desc Setup system to start a new game */ this.setModeReady = function() { this.lh.resetLog(); this.lh.logScream( "วางมือระบบ Censor ของ LEAP MOTION แบ แล้วกำมือ เพื่อเริ่มเกม" ); this.mode = AllHandsSign.MODE_READY; } /** * @desc Setup system countdown */ this.countToPlay = function() { this.lh.resetLog(); this.lh.logScream( "พร้อมเล่นเกม" ); this.countTID = window.setInterval( "rps.countDownStep();", 1000 ); this.mode = AllHandsSign.MODE_321; } /** * @desc Execute each step of a countdown */ this.countDownStep = function() { this.lh.resetLog(); if( this.count == 0 ) { this.lh.logScream( "Go." ); window.clearInterval( this.countTID ); this.setMode( AllHandsSign.MODE_LISTEN ); } else { if( this.count == 3 ) this.lh.resetLog(); this.lh.logScream( this.count + ". " ); this.count --; } } /** * @desc Setup system to listen user hand */ this.setModeListen = function() { window.clearTimeout( this.answerTID ); this.answerTID = window.setTimeout( "rps.listenTimeout();", AllHandsSign.ANSWER_TIME_OUT ); window.setTimeout( "rps.cpuChoose();", Math.random() * AllHandsSign.ANSWER_TIME_OUT ); this.mode = AllHandsSign.MODE_LISTEN; } /** * @desc Put the CPU hand in a turn. */ this.cpuChoose = function() { if( this.dumbuserHand ) { // user is dumb. Must be punished. CPU saw. this.cpuChoice = this.getFulminantAttack( this.dumbuserHand ); } else { this.cpuChoice = Math.round( Math.random() * 2 ); } if( this.userChoice > -1 ) { this.setMode( AllHandsSign.MODE_EVALUATE ); } } /** * @desc Fired when user takes too long time to answer. */ this.listenTimeout = function() { this.userLosetime = true; window.clearTimeout( this.answerTID ); this.lh.resetLog(); this.lh.logScream( "You lose.<br />Be faster!" ); this.cpuScore ++; this.appendResults(); window.clearTimeout( this.listenTimeout ); window.setTimeout( "rps.nextRound();", AllHandsSign.NEXTROUND_TIME_OUT ); } /** * @desc Fired when system confirms a valid user hand. */ this.listenCatchedHand = function( hand ) { window.clearTimeout( this.answerTID ); this.userChoice = hand; console.log( "DETECT! " + hand ); if( this.cpuChoice > -1 ) { this.setMode( AllHandsSign.MODE_EVALUATE ); } } /** * @desc Prepare state machine for next round */ this.nextRound = function() { this.count = 3; this.dumbuserHand = null; this.handHistory = new Array(); this.cpuChoice = -1; this.userChoice = -1; this.userLosetime = false; this.setMode( AllHandsSign.MODE_321 ); } this.appendResults = function() { this.lh.logScream( "ผู้เล่น: " + this.playerScore + " / ศัตรู: " + this.cpuScore ); } /** * @desc System looks at USER and CPU hands to decide the winner */ this.setModeEvaluate = function() { this.lh.resetLog(); this.lh.logScream( "<div>ผู้เล่น: <br/><img src='images/" + this.getHandname( this.userChoice ) + ".png'/></div><div>ศัตรู: <br/><img src='images/e_" + this.getHandname( this.cpuChoice )+".png' /></br/><br/>" ); var userWon = ( ( this.userChoice == AllHandsSign.ROCK && this.cpuChoice == AllHandsSign.SCISSORS ) || ( this.userChoice == AllHandsSign.PAPER && this.cpuChoice == AllHandsSign.ROCK ) || ( this.userChoice == AllHandsSign.SCISSORS && this.cpuChoice == AllHandsSign.PAPER ) ); var cpuWon = ( ( this.cpuChoice == AllHandsSign.ROCK && this.userChoice == AllHandsSign.SCISSORS ) || ( this.cpuChoice == AllHandsSign.PAPER && this.userChoice == AllHandsSign.ROCK ) || ( this.cpuChoice == AllHandsSign.SCISSORS && this.userChoice == AllHandsSign.PAPER ) ); if( userWon ) { this.playerScore ++; this.lh.logScream( "คุณ ชนะ" ); } else if( cpuWon ) { this.cpuScore ++; this.lh.logScream( "คุณ แพ้" ); } else { this.lh.logScream( "เสมอ!" ); } this.appendResults(); window.setTimeout( "rps.nextRound();", AllHandsSign.NEXTROUND_TIME_OUT ); this.mode = AllHandsSign.MODE_EVALUATE; } /** * @desc Debug method to debug Helper */ this.showResult = function() { var pointableCount = this.lh.getPointableCountByHand(); if( pointableCount.hands.length == 0 ) { this.lh.logScream( "There is<br />no hands." ); } else { for( var hand in pointableCount.hands ) { this.lh.logScream( "Hand " + ( parseInt( hand ) + 1 ) + " is<br />" + this.checkHand( pointableCount.hands[ hand ] ) + "." ); } } } /** * @desc Main strategy to detect hands based on pointable count */ this.checkHand = function( hand ) { if( hand.pointableCount == 2 ) { return AllHandsSign.SCISSORS; } else if( hand.pointableCount > 2 ) { return AllHandsSign.PAPER; } else { return AllHandsSign.ROCK; } } this.getFulminantAttack = function( hand ) { switch( hand ) { case AllHandsSign.ROCK: return AllHandsSign.PAPER; case AllHandsSign.PAPER: return AllHandsSign.SCISSORS; case AllHandsSign.SCISSORS: return AllHandsSign.ROCK; } } /** * @desc Used to get a name for each hand position */ this.getHandname = function( hand ) { switch( hand ) { case AllHandsSign.ROCK: return "rock"; case AllHandsSign.PAPER: return "paper"; case AllHandsSign.SCISSORS: return "scissors"; } } }
โดยการทำงานนั้นจะมีการทำงานดังนี้
this.setModeReady = function() { this.lh.resetLog(); this.lh.logScream( "วางมือระบบ Censor ของ LEAP MOTION แบ แล้วกำมือ เพื่อเริ่มเกม" ); this.mode = AllHandsSign.MODE_READY; } /** * @desc Setup system countdown */ this.countToPlay = function() { this.lh.resetLog(); this.lh.logScream( "พร้อมเล่นเกม" ); this.countTID = window.setInterval( "rps.countDownStep();", 1000 ); this.mode = AllHandsSign.MODE_321; }
setModeReady คือการ Detect มือเราว่าพร้อมสำหรับเล่นเกมหรือเปล่าโดยการ แบ มือไว้ก่อนแล้วทำการ กำมือให้เรียบร้อย ก็จะเข้าสู่โหมดการนับถอยหลังคือ countToPlay เพื่อเล่นเกม เป่า ยิ๊ง ฉุบ ต่อกับระบบ AI ที่สุ่มค่าออกมา
ส่วนของการเล่นเกมคือ ส่วน ตรงนี้ครับ เป็นการเก็บค่า มือ ของเราจาก บทเรียนก่อนหน้านี้: Leap Motion กับการ Detect Hand หรือมือของเรา
เมื่อได้ค่ามาจะทำการแปลงเป็นเงื่อนไขตัวเลขตามทฤษฏี Computation Theory ปรกติๆ
this.appendResults = function() { this.lh.logScream( "ผู้เล่น: " + this.playerScore + " / ศัตรู: " + this.cpuScore ); } /** * @desc System looks at USER and CPU hands to decide the winner */ this.setModeEvaluate = function() { this.lh.resetLog(); this.lh.logScream( "<div>ผู้เล่น: <br/><img src='images/" + this.getHandname( this.userChoice ) + ".png'/></div><div>ศัตรู: <br/><img src='images/e_" + this.getHandname( this.cpuChoice )+".png' /></br/><br/>" ); var userWon = ( ( this.userChoice == AllHandsSign.ROCK && this.cpuChoice == AllHandsSign.SCISSORS ) || ( this.userChoice == AllHandsSign.PAPER && this.cpuChoice == AllHandsSign.ROCK ) || ( this.userChoice == AllHandsSign.SCISSORS && this.cpuChoice == AllHandsSign.PAPER ) ); var cpuWon = ( ( this.cpuChoice == AllHandsSign.ROCK && this.userChoice == AllHandsSign.SCISSORS ) || ( this.cpuChoice == AllHandsSign.PAPER && this.userChoice == AllHandsSign.ROCK ) || ( this.cpuChoice == AllHandsSign.SCISSORS && this.userChoice == AllHandsSign.PAPER ) ); if( userWon ) { this.playerScore ++; this.lh.logScream( "คุณ ชนะ" ); } else if( cpuWon ) { this.cpuScore ++; this.lh.logScream( "คุณ แพ้" ); } else { this.lh.logScream( "เสมอ!" ); } this.appendResults(); window.setTimeout( "rps.nextRound();", AllHandsSign.NEXTROUND_TIME_OUT ); this.mode = AllHandsSign.MODE_EVALUATE; }
โดยเราจะใช้ตัวแปรรับค่า paper, scissors และ rock มาเทียบกับรูปภาพเหล่านี้ครับ โดยตัวสีฟ้าคือ มือของเรา และ สีแดงคือ ศัตรู (ใช้ e_ นำหน้าไฟล์ครอบอีกที)
ไปแก้ไข index.html ด้วยการเพิ่ม
<script type="text/javascript" src="function.js"></script>
และ ส่วนนี้ก่อน <body>
<script> var ws; //Web Server if ((typeof(WebSocket) == 'undefined') && (typeof(MozWebSocket) != 'undefined')) { WebSocket = MozWebSocket; } //event handlers function init() { //Create and open the socket ws = new WebSocket("ws://localhost:6437/"); //successful ws.onopen = function(event) { document.getElementById("game").style.visibility = "visible"; document.getElementById("connection").innerHTML = "Device Connected"; rps.init(); }; ws.onmessage = function(event) { var obj = JSON.parse(event.data); var str = JSON.stringify(obj, undefined, 2); if( rps ) { rps.lh.setFrame( obj ); } }; // On socket close ws.onclose = function(event) { ws = null; document.getElementById("game").style.visibility = "hidden"; document.getElementById("connection").innerHTML = "Device Connected"; } //On socket error ws.onerror = function(event) { alert("Received error"); }; } rps = new AllHandsSign(); AllHandsSign.ANSWER_TIME_OUT = 900; AllHandsSign.NEXTROUND_TIME_OUT = 4000; AllHandsSign.MOVEMENT_CATCH_COUNT = 4; // forms AllHandsSign.ROCK = 0; AllHandsSign.PAPER = 1; AllHandsSign.SCISSORS = 2; // State machine controls AllHandsSign.MODE_READY = 0; AllHandsSign.MODE_321 = 1; AllHandsSign.MODE_LISTEN = 2; AllHandsSign.MODE_EVALUATE = 3; </script>
ทำการ Run ตัว HTML ของเรา พร้อมต่อ Leap Motion เข้ากับเครื่องคอมพิวเตอร์ของเรา
Source code: สามารถเข้าไปที่ GitHub ของ nandico ตรงก็ได้ หรือ ใช้ตัวที่ขอ อนุญาติ Repo มาแล้วที่ https://www.daydev.com/download/repository-js-leap.zip