Skip to content

Instantly share code, notes, and snippets.

@e1blue
Created June 20, 2018 02:23
Show Gist options
  • Select an option

  • Save e1blue/bacdcdad6015b86e173d44ccb720e1d0 to your computer and use it in GitHub Desktop.

Select an option

Save e1blue/bacdcdad6015b86e173d44ccb720e1d0 to your computer and use it in GitHub Desktop.
Pacman Canvas + TweenMax
canvas#mycanvas
//環境變數
var updateFPS = 30
var showMouse = true
var time = 0
var bgColor ="black"
//控制
var controls = {
value: 0
}
var gui = new dat.GUI()
gui.add(controls,"value",-2,2).step(0.01).onChange(function(value){})
let PI = n => n === undefined? Math.PI : Math.PI*n
//------------------------
// Vec2
class Vec2{
constructor(x,y){
this.x = x
this.y = y
}
set(x,y){
this.x =x
this.y =y
}
setV(v){
this.x=v.x
this.y=v.y
}
move(x,y){
this.x+=x
this.y+=y
}
add(v){
return new Vec2(this.x+v.x,this.y+v.y)
}
sub(v){
return new Vec2(this.x-v.x,this.y-v.y)
}
mul(s){
return new Vec2(this.x*s,this.y*s)
}
get length(){
return Math.sqrt(this.x*this.x+this.y*this.y)
}
set length(nv){
let temp = this.unit.mul(nv)
this.set(temp.x,temp.y)
}
clone(){
return new Vec2(this.x,this.y)
}
toString(){
return `(${this.x}, ${this.y})`
}
equal(v){
return this.x==v.x && this.y ==v.y
}
get angle(){
return Math.atan2(this.y,this.x)
}
get unit(){
return this.mul(1/this.length)
}
static get ZERO(){
return new Vec2(0, 0)
}
static get UP(){
return new Vec2(0,-1)
}
static get DOWN(){
return new Vec2(0,1)
}
static get LEFT(){
return new Vec2(-1,0)
}
static get RIGHT(){
return new Vec2(1,0)
}
static DIR(str){
if (!str) {
return Vec2.ZERO
}
let type = (""+str).toUpperCase()
return Vec2[type]
}
static DIR_ANGLE(str){
switch(str){
case "right":
return 0
case "left":
return PI()
case "up":
return PI(-0.5)
case "down":
return PI(0.5)
}
return 0
}
}
//------
var canvas = document.getElementById("mycanvas")
var ctx = canvas.getContext("2d")
circle= function(v,r){
ctx.arc(v.x,v.y,r,0,PI(2))
}
line= function(v1,v2){
ctx.moveTo(v1.x,v1.y)
ctx.lineTo(v2.x,v2.y)
}
let getVec2 = (args)=>{
if (args.length==1){
return args[0]
}else if (args.length==2){
return new Vec2(args[0],args[1])
}
}
let moveTo = function(){
let v = getVec2(arguments)
ctx.moveTo(v.x,v.y)
}
let lineTo = function(){
let v = getVec2(arguments)
ctx.lineTo(v.x,v.y)
}
let translate = function(){
let v = getVec2(arguments)
ctx.translate(v.x,v.y)
}
let arc = function(){
ctx.arc.apply(ctx,arguments)
}
let rotate = (angle)=>{
if(angle!=0){
ctx.rotate(angle)
}
}
let beginPath = ()=>{ctx.beginPath()}
let closePath = ()=>{ctx.closePath()}
let setFill = (color)=>{ ctx.fillStyle=color }
let setStroke = (color)=>{ ctx.strokeStyle=color }
let fill = (color)=>{
if(color){
setFill(color)
}
ctx.fill()
}
let stroke = (color)=>{
if(color){
ctx.strokeStyle=color
}
ctx.stroke()
}
let save = (func)=>{
ctx.save()
func()
ctx.restore()
}
function initCanvas(){
ww = canvas.width = window.innerWidth
wh = canvas.height = window.innerHeight
}
initCanvas()
//定義格子大小跟如何取得位置
var WSPAN = Math.min(ww,wh)/24
function GETPOS(i,o){
let sourceV = getVec2(arguments)
return sourceV
.mul(WSPAN)
.add(new Vec2(WSPAN/2,WSPAN/2))
}
//定義遊戲內物件原型
class GameObject{
constructor(args){
let def = {
p: new Vec2(0,0),
gridP: new Vec2(1,1),
}
Object.assign(def,args)
Object.assign(this,def)
this.p = GETPOS(this.gridP)
}
collide(gobj){
return this.p.sub(gobj.p).length < WSPAN
}
}
class Player extends GameObject{
constructor(args){
super(args)
let def = {
nextDirection: null,
currentDirection: null,
isMoving: false,
speed: 40
}
Object.assign(def,args)
Object.assign(this,def)
}
draw(){
beginPath()
circle(this.p,5)
fill("white")
}
get directionAngle(){
return Vec2.DIR_ANGLE(this.currentDirection)
}
moveStep(){
//處理移動
let i0 = this.gridP.x,
o0 = this.gridP.y
let oldDirection = this.currentDirection
let haveWall = map.getWalls(this.gridP.x,this.gridP.y)
//查看能移動的方向
let avail = ['up','down','left','right']
.filter(d=>!haveWall[d])
//如果下一個指定方向沒有牆面,就更新方向
if (!haveWall[this.nextDirection]){
this.currentDirection=this.nextDirection
}
//根據方向更新位置
this.gridP=this.gridP.add(Vec2.DIR(this.currentDirection))
let isWall = map.isWall(this.gridP.x,this.gridP.y)
if (!isWall){
this.isMoving=true
let moveStepTime = 10/this.speed
//如果在左右邊界且符合方向 => 瞬間跳躍
if (this.gridP.x<=-1 && this.currentDirection=='left'){
this.gridP.x=18
moveStepTime=0
}
if (this.gridP.x>=19 && this.currentDirection=='right'){
this.gridP.x=0
moveStepTime=0
}
//製作移動
TweenMax.to(this.p,moveStepTime,{
...GETPOS(this.gridP),
ease: Linear.easeNone,
onComplete: ()=>{
this.isMoving=false
this.moveStep()
}
} )
return true
}else{
//如果下一個方向是墻就倒退回原始位置,維持原始方向前進
this.gridP.set(i0,o0)
this.currentDirection = oldDirection
}
}
}
class Pacman extends Player{
constructor(args){
super(args)
let def = {
deg: Math.PI/4,
r: 50,
deadDeg: null,
isDead: false
}
Object.assign(def,args)
Object.assign(this,def)
}
update(){
if (this.isDead){
this.isMoving=false
}
}
draw(){
let useDeg = Math.PI/4
if (this.isMoving){
useDeg = this.deg
}
if (this.deadDeg){
useDeg = this.deadDeg
}
save(()=>{
translate(this.p)
moveTo(Vec2.ZERO)
rotate(this.directionAngle)
rotate(useDeg)
lineTo(this.r,0)
arc(0,0,this.r,0,2*Math.PI-useDeg*2)
closePath()
fill("yellow")
})
}
die(){
if (!this.isDead){
//重置所有動畫
TweenMax.killAll()
this.isDead=true
this.deadDeg=Math.PI
//張開嘴
TweenMax.from(this,1.5,{
deadDeg: 0,
ease: Linear.easeNone,
delay: 1
})
}
}
}
class Food extends GameObject{
constructor(args){
super(args)
let def = {
eaten: false,
super: false
}
Object.assign(def,args)
Object.assign(this,def)
}
draw(){
if (!this.eaten){
save(()=>{
translate(this.p)
setFill("#f99595")
if (this.super){
//閃爍
if (time%20<10){
beginPath()
setFill("white")
arc(0,0,WSPAN/5,0,PI(2))
fill()
}
}else{
ctx.fillRect(-WSPAN/10,-WSPAN/10,WSPAN/5,WSPAN/5)
}
})
}
}
}
class Ghost extends Player{
constructor(args){
super(args)
let def = {
r: 50,
color: "red",
isEatable: false,
isDead: false,
eatableCounter: 0,
traceGoCondition: [
{
name: 'left', condition:(target)=> (this.gridP.x>target.x),
},
{
name: 'right', condition:(target)=> (this.gridP.x<target.x),
},
{
name: 'up', condition:(target)=> (this.gridP.y>target.y),
},
{
name: 'down', condition:(target)=> (this.gridP.y<target.y)
},
]
}
Object.assign(def,args)
Object.assign(this,def)
}
update(){
this.speed=38
if (this.isEatable) this.speed=25
if (this.isDead) this.speed=80
if (this.isDead && this.gridP.equal(new Vec2(9,9)) ){
this.reLive()
}
}
draw(){
save(()=>{
translate(this.p)
if (!this.isDead){
beginPath()
//身體上半
arc(0,0,this.r,PI(),0)
lineTo(this.r,this.r)
let tt = parseInt(time/3)
let ttSpan = this.r*2/7
let ttHeight = this.r/3
//鋸齒狀
for(var i=0;i<7;i++){
ctx.lineTo(this.r*0.9-ttSpan*i,this.r+((i+tt)%2)*-ttHeight)
}
ctx.lineTo(-this.r,this.r)
setFill( !this.isEatable?this.color:((time%10<5 || this.isEatableCounter>3)?"#1f37ef":"#fff"))
fill()
}
let hasEye = !this.isEatable || this.isDead
//眼球跟眼睛大小
let eyeR = this.r/3
let innerEyeR = eyeR/2
if (hasEye){
//eye shape
beginPath()
arc(-this.r/2.5,-eyeR,eyeR,0,PI(2))
arc( this.r/2.5,-eyeR,eyeR,0,PI(2))
fill("white")
}
//畫上眼球
save(()=>{
beginPath()
let innerEyePan = (Vec2.DIR(this.currentDirection)).mul(2)
translate(innerEyePan)
arc(-this.r/2.5,-eyeR,innerEyeR,0,PI(2))
arc(this.r/2.5,-eyeR,innerEyeR,0,PI(2))
fill( hasEye ?"black":"white")
})
})
}
getNextDirection(map,pacman){
let currentTarget = this.isDead?(new Vec2(9,9)):pacman.gridP
let go = !this.isEatable || this.isDead
//留下應該前進的方向
let traceGo = this.traceGoCondition.filter(obj=> {
let cond = obj.condition(currentTarget)
return go? cond:!cond
}).map(obj=>obj.name)
///取得可走的方向
let haveWall = map.getWalls(this.gridP.x,this.gridP.y)
//過濾可以前進且應該前進的方向與反方向
let traceGoAndCanGo = traceGo
.filter(o=>!haveWall[o] )
.filter(nn=>
Vec2.DIR(nn).add(Vec2.DIR(this.currentDirection) ).length!=0
)
//過濾可走的方向
let availGo =['left','right','up','down']
.filter(d=>!haveWall[d])
//如果當下只有兩個方向,維持原本進行
if (availGo.length==2){
if ((haveWall.up && haveWall.down) ||
(haveWall.left && haveWall.right) ){
return this.currentDirection
}
}
//優先走該走方向,如果無就從可走方向挑
let finalPossibleSets = traceGoAndCanGo.length?traceGoAndCanGo:availGo
let finalDecision = finalPossibleSets[ parseInt(Math.random()*finalPossibleSets.length) ] || 'top'
return finalDecision
}
die(){
this.isDead=true
}
reLive(){
this.isDead=false
this.isEatable=false
}
setEatable(time){
this.isEatableCounter=time
if (!this.isEatable){
this.isEatable=true
//設定倒數計時
let func = (()=>{
this.isEatableCounter--
if (this.isEatableCounter<=0){
this.isEatable=false
}else{
setTimeout(func,1000)
}
})
func()
}
}
}
class Map {
constructor(){
this.mapData = [
"ooooooooooooooooooo",
"o o o",
"o oo ooo o ooo oo o",
"o+ +o",
"o oo o ooooo o oo o",
"o o o o o",
"oooo ooo o ooo oooo",
"xxxo o o oxxx",
"oooo o oo oo o oooo",
" oxxxo ",
"oooo o ooooo o oooo",
"xxxo o x o oxxx",
"oooo ooo o ooo oooo",
"o o o o o",
"o oo o ooooo o oo o",
"o+ +o",
"o oo ooo o ooo oo o",
"o o o",
"ooooooooooooooooooo",
]
this.init()
}
init(){
this.pacman = new Pacman({
gridP: new Vec2(9,11),
r: WSPAN/2
})
TweenMax.to(this.pacman,0.15,{deg: 0,ease: Linear.easeNone,repeat: -1,yoyo: true})
this.ghosts = Array.from({length: 4},(d,i)=>
new Ghost({
gridP: new Vec2(9+i%2,9),
r: WSPAN/2*0.9,
color: ["red","#ffa928","#16ebff","#ff87ab"][i%4]
})
)
this.foods=[]
for(let i=0;i<20;i++){
for(let o=0;o<20;o++){
let foodType=this.isFood(i,o)
if (foodType){
let food = new Food({
gridP: new Vec2(i,o),
super: foodType.super
})
this.foods.push(food)
}
}
}
}
draw(){
for(let i=0;i<19;i++){
for(let o=0;o<19;o++){
save(()=>{
translate(GETPOS(i,o))
ctx.strokeStyle="rgba(255,255,255,0.5)"
// ctx.strokeRect(-WSPAN/2,-WSPAN/2,WSPAN,WSPAN)
let walltype = this.getWalls(i,o)
setStroke("blue")
ctx.shadowColor = "rgba(30,30,255)"
ctx.shadowBlur = 30
ctx.lineWidth=WSPAN/5
let typecode = ['up','down','left','right']
.map(d=>walltype[d]?1:0)
.join("")
typecode=walltype.none?"":typecode
let countSide = (typecode.match(/1/g) || []).length
let wallSpan = WSPAN / 4.5
let wallLen = WSPAN / 2
if (typecode =="1100" || typecode=="0011"){
if (typecode == "0011"){
rotate(PI(0.5))
}
save(()=>{
beginPath()
moveTo(wallSpan,-wallLen)
lineTo(wallSpan,wallLen)
moveTo(-wallSpan,-wallLen)
lineTo(-wallSpan,wallLen)
stroke()
})
}else if ( countSide==2 ){
let angles = {
'1010': 0, '1001': 0.5,
'0101': 1, '0110': 1.5
}
save(()=>{
rotate( PI(angles[typecode]) )
beginPath()
arc(-wallLen,-wallLen,wallLen+wallSpan,0,PI(0.5))
stroke()
beginPath()
arc(-wallLen,-wallLen,wallLen-wallSpan,0,PI(0.5))
stroke()
})
}
if ( countSide==1 ){
let angles = {
'1000': 0, '0001': 0.5,
'0100': 1, '0010': 1.5
}
save(()=>{
rotate( PI(angles[typecode]) )
beginPath()
arc(0,0,wallSpan,0,PI())
stroke()
beginPath()
moveTo(wallSpan, -wallLen)
lineTo(wallSpan, 0)
moveTo(-wallSpan, -wallLen)
lineTo(-wallSpan, 0)
stroke()
})
}
if (countSide==3){
let angles = {
'1011': 0, '1101': 0.5,
'0111': 1, '1110': 1.5
}
save(()=>{
rotate( PI( angles[typecode] ) )
beginPath()
arc(-wallLen,-wallLen,wallLen-wallSpan,0,PI(0.5))
stroke()
beginPath()
arc(wallLen,-wallLen,wallLen-wallSpan,-PI(1.5),-PI(1))
stroke()
beginPath()
moveTo(-wallLen,wallSpan)
lineTo(wallLen,wallSpan)
stroke()
})
}
})
}
}
}
getWallContent(o,i){
//map array and reverse direction
return this.mapData[i] && this.mapData[i][o]
}
isWall(i,o){
let type = this.getWallContent(i,o)
return type=="o"
}
getWalls(i,o){
return {
up: this.isWall(i,o-1),
down: this.isWall(i,o+1),
left: this.isWall(i-1,o),
right: this.isWall(i+1,o),
none: !this.isWall(i,o)
}
}
isFood(i,o){
let type = this.getWallContent(i,o)
if (type=="+" || type==" "){
return {
super: type=="+"
}
}
return false
}
}
var map
function init(){
map = new Map()
}
function update(){
time++
map.ghosts.forEach(ghost=>{
ghost.update()
//鬼魂遊走策略
ghost.nextDirection=ghost.getNextDirection(map,map.pacman)
if (!ghost.isMoving){
ghost.moveStep()
}
//如果有碰撞
if (!ghost.isDead && !map.pacman.isDead && ghost.collide(map.pacman)){
if (!ghost.isEatable){
//鬼吃小精靈
map.pacman.die()
setTimeout(()=>{
map.init()
},4000)
}else {
//小精靈吃鬼
ghost.die()
//暫停
TweenMax.pauseAll()
setTimeout(()=>{
TweenMax.resumeAll()
},500)
}
}
})
//吃食物判斷 檢查最近的幾顆
let currentFood = map.foods.find(food=>
food.gridP.sub(map.pacman.gridP).length<=3 &&
food.p.sub(map.pacman.p).length<=WSPAN/2 )
//檢查是否可以吃
if (currentFood && !currentFood.eaten){
currentFood.eaten=true
//超級食物
if (currentFood.super){
//沒死的鬼變成可以吃
map.ghosts.filter(ghost=>!ghost.isDead)
.forEach(ghost=>{
ghost.setEatable(10)
})
}
}
}
function draw(){
//清空背景
setFill(bgColor)
ctx.fillRect(0,0,ww,wh)
//-------------------------
// 在這裡繪製
save(()=>{
translate(ww/2-WSPAN*10,wh/2-WSPAN*10)
map.draw()
map.foods.forEach(food=>food.draw())
map.pacman.draw()
map.ghosts.forEach(ghost=>ghost.draw())
setFill('white')
ctx.font="20px Ariel"
ctx.fillText("Score: "+ map.foods.filter(f=>f.eaten).length*10,0,-10)
})
//-----------------------
//繪製滑鼠座標
setFill("red")
beginPath()
circle(mousePos,2)
fill()
save(()=>{
beginPath()
translate(mousePos)
setStroke("red")
let len = 20
line(new Vec2(-len,0),new Vec2(len,0))
line(new Vec2(0,-len),new Vec2(0,len))
ctx.fillText(mousePos,10,-10)
stroke()
})
//schedule next render
requestAnimationFrame(draw)
}
function loaded(){
initCanvas()
init()
requestAnimationFrame(draw)
setInterval(update,1000/updateFPS)
}
window.addEventListener("load",loaded)
window.addEventListener("resize",initCanvas)
//滑鼠事件跟紀錄
var mousePos = new Vec2(0,0)
var mousePosDown = new Vec2(0,0)
var mousePosUp = new Vec2(0,0)
window.addEventListener("mousemove",mousemove)
window.addEventListener("mouseup",mouseup)
window.addEventListener("mousedown",mousedown)
function mousemove(evt){
mousePos.setV(evt)
// console.log(mousePos)
}
function mouseup(evt){
mousePos.setV(evt)
mousePosUp = mousePos.clone()
}
function mousedown(evt){
mousePos.setV(evt)
mousePosDown = mousePos.clone()
}
window.addEventListener("keydown",function(evt){
//設定小精靈方向
if (!map.pacman.isDead){
map.pacman.nextDirection=evt.key.replace("Arrow","").toLowerCase()
if (!map.pacman.isMoving){
map.pacman.moveStep()
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.1/dat.gui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenMax.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.js"></script>
html,body
margin: 0
height: 100%
overflow: hidden
canvas
width: 100%
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment