DeveloperFeaturedJavaScriptLeap MotionProgramming Language

LEAP MOTION กับ JS สร้างเกม เป่า ยิ๊ง ฉุบ Rock Paper Scissors

ตัวอย่างการสร้างเกม 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_ นำหน้าไฟล์ครอบอีกที)

e_paper.png
e_paper.png
e_rock.png
e_rock.png
e_scissors.png
e_scissors.png
paper.png
paper.png
rock.png
rock.png
scissors.png
scissors.png

ไปแก้ไข 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 เข้ากับเครื่องคอมพิวเตอร์ของเรา

ต่อ Leap Motion กับเครื่องซะ
ต่อ Leap Motion กับเครื่องซะ
เริ่มเกมโดย แบมือเหนือ Leap motion แล้ว กำมือ
เริ่มเกมโดย แบมือเหนือ Leap motion แล้ว กำมือ
ทดสอบการเล่นเกม
ทดสอบการเล่นเกม
มีค่าความคลาดเคลื่อนของ กรรไกร กับ ค้อน บ่อยเล็กน้อย
มีค่าความคลาดเคลื่อนของ กรรไกร กับ ค้อน บ่อยเล็กน้อย

Source code: สามารถเข้าไปที่ GitHub ของ nandico ตรงก็ได้ หรือ ใช้ตัวที่ขอ อนุญาติ Repo มาแล้วที่ https://www.daydev.com/download/repository-js-leap.zip

Asst. Prof. Banyapon Poolsawas

อาจารย์ประจำสาขาวิชาการออกแบบเชิงโต้ตอบ และการพัฒนาเกม วิทยาลัยครีเอทีฟดีไซน์ & เอ็นเตอร์เทนเมนต์เทคโนโลยี มหาวิทยาลัยธุรกิจบัณฑิตย์ ผู้ก่อตั้ง บริษัท Daydev Co., Ltd, (เดย์เดฟ จำกัด)

Related Articles

Back to top button

Adblock Detected

เราตรวจพบว่าคุณใช้ Adblock บนบราวเซอร์ของคุณ,กรุณาปิดระบบ Adblock ก่อนเข้าอ่าน Content ของเรานะครับ, ถือว่าช่วยเหลือกัน