欧美成人午夜精品久久久,国产?V天堂一区二区三区,欧美精品va在线观看,亚洲一区二区三区免费在线观看,av无码精品一区二区久久,欧美性爱视频不卡一区三区,欧美乱人伦视频在线观看,国产一级牲交高潮

ARTICLE DETAIL

資訊詳情

深耕商務(wù)建站與企業(yè)官網(wǎng)運營的一線實戰(zhàn)洞察。

用TypeScript類組織貪吃蛇:面向?qū)ο笥螒蜷_發(fā)實戰(zhàn)

用TypeScript類組織貪吃蛇:面向?qū)ο笥螒蜷_發(fā)實戰(zhàn) 1. 整體設(shè)計思路為什么用類來組織貪吃蛇我見過不少初學(xué)者寫貪吃蛇習(xí)慣把所有邏輯塞進一個 main 函數(shù)或者全用全局變量堆。全局變量一多代碼就成了一鍋粥。這次我們用 TypeScript 的類來組織但很多人會問一句貪吃蛇這么小有必要搞類嗎我覺得有必要。不是小題大做而是貪吃蛇這個項目其實覆蓋了面向?qū)ο笤O(shè)計里最典型的幾個問題誰負責(zé)數(shù)據(jù)、誰負責(zé)行為、誰負責(zé)協(xié)調(diào)、誰負責(zé)渲染。如果一開始就把職責(zé)劃清楚后面加計分、加速、障礙物這些功能都會順手很多。整款游戲的核心邏輯可以拆成三大塊。第一塊是蛇類負責(zé)蛇的狀態(tài)、移動規(guī)則、碰撞自檢、以及自身的繪制。第二塊是食物類負責(zé)食物坐標的隨機生成與繪制。第三塊是游戲控制器類負責(zé)把上面兩塊組合起來再承擔(dān)游戲循環(huán)、分數(shù)結(jié)算、鍵盤監(jiān)聽、開始結(jié)束狀態(tài)的切換。project-root/ ├─ src/ │ ├─ type.ts # 共享類型定義 │ ├─ food.ts # 食物類 │ ├─ snake.ts # 蛇類 │ ├─ game.ts # 游戲控制器類 │ └─ index.ts # 入口文件 ├─ index.html └─ tsconfig.json這個拆分有一個好處每個文件只做一件事。type.ts 里沒有邏輯只放類型snake.ts 里不碰鍵盤事件game.ts 里不關(guān)心蛇身格子到底是什么顏色。將來你如果想換成貪吃蛇雙人對戰(zhàn)版基本只需要創(chuàng)建兩個 Snake 實例再改一下控制器——這就是低耦合帶來的實際收益。1.1 類型先行先定義數(shù)據(jù)形狀再寫邏輯寫 TypeScript 項目我習(xí)慣的第一步永遠是定義類型。別看類型定義只是幾行 interface它相當(dāng)于整個項目的地基。地基歪了上面蓋什么都是歪的。貪吃蛇這個項目我認為最少需要這三樣類型/** 方向枚舉 */ export enum Direction { Up Up, Down Down, Left Left, Right Right } /** 坐標結(jié)構(gòu) */ export interface Point { x: number y: number } /** 蛇身和食物的通用格子結(jié)構(gòu) */ export interface Cell { x: number y: number }方向用枚舉還是用字符串字面量聯(lián)合類型都可以但我個人更推薦枚舉。原因很實在枚舉給每個方向一個語義名字代碼里不會出現(xiàn)裸字符串。你想想如果有人寫snake.faceTo(left)一旦字母拼錯整個方向判斷就廢了。用Direction.Left就完全沒這個問題編輯器還能給你直接提示省去翻代碼確認的麻煩。坐標為什么要分開 Point 和 Cell嚴格來說這兩個都是{ x: number; y: number }但語義完全不同。Point 強調(diào)空間上的一個點Cell 強調(diào)的是畫布上的一個格子。如果你在代碼里直接混用看moveTo(point)和moveTo(cell)的時候你很難一眼判斷參數(shù)到底是什么意思。TypeScript 是結(jié)構(gòu)化類型系統(tǒng)兩個形狀相同的接口是可以互相賦值的但它們的存在本身就在提醒你——這里的坐標代表什么含義。1.2 配置參數(shù)集中管理別寫死魔法數(shù)字寫游戲最忌諱的就是各種數(shù)字散落在代碼的各個角落比如畫布寬度 400、格子大小 20、蛇初始長度 3這些魔法數(shù)字如果你在每個方法里重復(fù)寫后期想要調(diào)整的時候得滿文件去搜。像我之前做某個項目時就吃過這種虧改一個參數(shù)要按三遍 CtrlF。我建議的做法是直接在 type.ts 里定一個配置對象/** 游戲參數(shù)配置 */ export const CONFIG { canvasWidth: 600, canvasHeight: 600, gridSize: 20, snakeInitLength: 4, snakeSpeed: 150, // 單位毫秒數(shù)值越大越慢 colorSnakeHead: #2d5016, colorSnakeBody: #4e8d3f, colorFood: #d1483b, colorBg: #f5f2e6 } as constas const會讓這些值的類型變成字面量類型比如canvasWidth的類型就是600而不是number。這有什么好處好處是如果你后續(xù)不小心寫了一個CONFIG.canvasWidth 800TypeScript 會直接在編譯階段報錯提醒你正在篡改一個只讀配置。對游戲這類運行時狀態(tài)變化頻繁的程序來說這個保護很值。還有一個細節(jié)蛇的移動速度我用snakeSpeed: 150表示每 150 毫秒走一格。這個值直接決定了游戲的難度。100 毫秒手感偏快200 毫秒手感偏慢150 毫秒是我調(diào)試下來比較舒服的檔位。如果你的玩家群體是小朋友建議放寬到 200。2. 食物類把地基打扎實很多人覺得食物類太簡單了就是把一個點隨機放在畫布上然后畫個圓。但食類有幾個細節(jié)值得琢磨比如隨機生成的坐標必須對齊網(wǎng)格否則蛇頭永遠吃不到食物食物不能生成在蛇身上否則一開局就穿模食物被吃掉后要能重新生成。2.1 食物類的職責(zé)范圍食物類的職責(zé)我限定為兩件生成一個當(dāng)前場景內(nèi)有效的坐標以及把自己畫在畫布上。它不關(guān)心蛇在哪里也不關(guān)心分數(shù)怎么加。那我怎么保證食物不生成在蛇身上答案是通過生成方法接收一個不可用區(qū)域參數(shù)由調(diào)用方來告訴它哪些坐標不能選。這個設(shè)計叫做依賴倒置。食物類并不直接引用 Snake 類型而是接收一個坐標數(shù)組。這樣食物類就不依賴蛇類將來如果棋盤上有障礙物也只需要把障礙物的坐標一并傳進來食物類不需要改一個字符。import { CONFIG } from ./type /** 食物類 */ export class Food { position: { x: number; y: number } constructor() { this.position { x: 0, y: 0 } } /** * 生成一個不包含在 forbiddenCells 里的隨機坐標 * param forbiddenCells 不可選的坐標蛇身等 */ generate(forbiddenCells: { x: number; y: number }[] []): void { // 計算行列數(shù)避免坐標越界 const rowNum CONFIG.canvasHeight / CONFIG.gridSize const colNum CONFIG.canvasWidth / CONFIG.gridSize while (true) { // 隨機取 0 ~ rowNum-1 之間的整數(shù) const newX Math.floor(Math.random() * colNum) const newY Math.floor(Math.random() * rowNum) // 如果這個坐標沒人占用則確定位置 const conflict forbiddenCells.some( cell cell.x newX cell.y newY ) if (!conflict) { this.position { x: newX, y: newY } return } } } /** 繪制食物 */ draw(ctx: CanvasRenderingContext2D): void { const { x, y } this.position // 給食物留一點內(nèi)邊距不然看起來像粘在一起 const radius CONFIG.gridSize / 2 - 3 ctx.save() ctx.shadowColor CONFIG.colorFood ctx.shadowBlur 6 ctx.fillStyle CONFIG.colorFood ctx.beginPath() // 注意這里換算的是像素中心點 ctx.arc( x * CONFIG.gridSize CONFIG.gridSize / 2, y * CONFIG.gridSize CONFIG.gridSize / 2, radius, 0, Math.PI * 2 ) ctx.fill() ctx.restore() } }2.2 隨機生成算法為什么用 while(true)你看那個while (true)可能會有點慌這玩意兒不會死循環(huán)嗎理論上如果棋盤被蛇占滿了確實會死循環(huán)。但貪吃蛇的棋盤大小時 30 × 30一共 900 個格子蛇身要達到 900 格這個可能性在實際游戲中幾乎為零。就算真的發(fā)生了那也是勝利的場面——棋盤都被蛇占滿了游戲早該結(jié)束了。不過從工程嚴謹?shù)慕嵌葋碚f我會給這個循環(huán)加一個最大嘗試次數(shù)。這個習(xí)慣是我自己后期養(yǎng)成的因為哪怕概率再低一個無法終止的循環(huán)在代碼評審里都是個隱患。比如可以改成嘗試 1000 次還找不到空位就拋異常。但在教學(xué)項目里while(true)配合占位判斷已經(jīng)完全夠用注釋里寫清楚前提就行。2.3 食物的繪制技巧食物的繪制我用的是shadowBlur加光暈效果這是讓食物看起來有質(zhì)感的低成本方式。不透明度和陰影的配合讓它像一顆水果糖而不是一個干巴巴的色塊。這里有個繪制坐標系容易混的地方。游戲邏輯里的坐標是格子坐標一格是 20 像素。比如{ x: 3, y: 5 }表示從左上角開始橫著數(shù)第 4 格、豎著數(shù)第 6 格。但 canvas 繪圖用的是像素坐標所以繪制的時候必須做一個換算像素X 格子X × 格子大小 格子大小 / 2 像素Y 格子Y × 格子大小 格子大小 / 2加一半格子大小的原因是為了讓圓心落在格子的正中心。如果不加圖案會偏在格子的左上角視覺上看起來很別扭。這個換算方法在蛇類繪制里也會用到建議你把它當(dāng)成一個固定的心法記下來。3. 蛇類實現(xiàn)游戲的核心到重點了。蛇類是貪吃蛇整個代碼里最復(fù)雜的類核心就是那幾件事數(shù)據(jù)怎么存、移動怎么做、碰撞怎么檢測、自己會不會咬到自己、吃到食物怎么變長、以及怎么把自己畫出來。3.1 蛇的數(shù)據(jù)結(jié)構(gòu)怎么選蛇在棋盤上的形態(tài)是一連串連續(xù)格子而且隨時會在頭部新增一格尾部可能減少一格。這種頭部增、尾部減的操作典型的先進先出邏輯天然適合用隊列。在 JavaScript 里數(shù)組的unshift在頭部插入、pop在尾部刪除剛好模擬了最自然的思路。但這里有一個性能上的點在教學(xué)里我會特意提一下unshift的時間復(fù)雜度是 O(n)因為數(shù)組頭部插入會引發(fā)后續(xù)所有元素的內(nèi)存移動。貪吃蛇的蛇身長度通常在幾十格這個量級這一點性能損失完全可忽略。如果你貪吃蛇以后要做到 500 格以上可以考慮用雙向鏈表存儲蛇身。在教學(xué)項目里用數(shù)組就行容易理解也容易調(diào)試。蛇類還用一個direction字段記錄當(dāng)前朝向一個pendingDirection記錄下一次移動的方向。為什么要兩個方向因為玩家在一幀之內(nèi)可能連續(xù)按兩個方向鍵如果不做緩沖第二個按鍵會覆蓋第一個按鍵導(dǎo)致蛇沒有按玩家的意圖轉(zhuǎn)向。而如果按下即生效又會出現(xiàn)一個經(jīng)典的 bug蛇正在向左走玩家按了向上再快速按了向右蛇會直接掉頭——蛇不允許直接 180 度轉(zhuǎn)向這個約束后面細說。3.2 蛇類的 TypeScript 實現(xiàn)import { CONFIG, Direction } from ./type interface Cell { x: number y: number } /** 蛇類管理蛇身狀態(tài)、移動規(guī)則與碰撞檢測 */ export class Snake { /** 蛇身第 0 項為蛇頭 */ body: Cell[] /** 當(dāng)前移動方向 */ direction: Direction /** 緩沖方向本輪移動尚未消費的最新轉(zhuǎn)向指令 */ private pendingDirection: Direction /** 本輪是否已經(jīng)移動過防止一幀內(nèi)多次移動 */ private movedThisTurn: boolean constructor() { // 初始蛇身從中間向左延伸 const gridX CONFIG.canvasWidth / CONFIG.gridSize const gridY CONFIG.canvasHeight / CONFIG.gridSize const startX Math.floor(gridX / 2) const startY Math.floor(gridY / 2) this.body [] for (let i 0; i CONFIG.snakeInitLength; i) { this.body.push({ x: startX - i, y: startY }) } this.direction Direction.Right this.pendingDirection Direction.Right this.movedThisTurn false } /** 獲取蛇頭坐標 */ get head(): Cell { return this.body[0] } /** 設(shè)置下一次移動的方向帶轉(zhuǎn)向合法性校驗 */ setDirection(newDir: Direction): void { // 左、右是橫向上、下是縱向 const isOpposite (this.direction Direction.Up newDir Direction.Down) || (this.direction Direction.Down newDir Direction.Up) || (this.direction Direction.Left newDir Direction.Right) || (this.direction Direction.Right newDir Direction.Left) // 如果是相反方向忽略 if (isOpposite) return // 如果相同方向忽略 if (this.direction newDir) return // 如果這一輪已經(jīng)移動過只更新緩沖方向下一輪再消費 if (this.movedThisTurn) { this.pendingDirection newDir } else { // 這一輪還沒移動直接生效 this.direction newDir this.pendingDirection newDir } } /** 蛇移動一步 */ move(grow: boolean): void { const head this.body[0] let newHead: Cell { x: head.x, y: head.y } switch (this.direction) { case Direction.Up: newHead.y - 1 break case Direction.Down: newHead.y 1 break case Direction.Left: newHead.x - 1 break case Direction.Right: newHead.x 1 break } // 頭部插入新格子 this.body.unshift(newHead) // 只有吃到食物才保留尾部否則移除尾部 if (!grow) { this.body.pop() } // 修正蛇頭坐標到棋盤范圍內(nèi)撞墻的場景留給控制器處理 const rowNum CONFIG.canvasHeight / CONFIG.gridSize const colNum CONFIG.canvasWidth / CONFIG.gridSize newHead.x (newHead.x colNum) % colNum newHead.y (newHead.y rowNum) % rowNum this.body[0] newHead // 移動完成消費緩沖方向 this.direction this.pendingDirection this.movedThisTurn true } /** 是否與自身發(fā)生碰撞 */ isHitSelf(): boolean { const head this.body[0] // 從索引 1 開始檢查因為索引 0 就是頭本身 return this.body.slice(1).some(segment segment.x head.x segment.y head.y) } /** 蛇頭是否撞到邊界 */ isHitWall(): boolean { const head this.body[0] const rowNum CONFIG.canvasHeight / CONFIG.gridSize const colNum CONFIG.canvasWidth / CONFIG.gridSize return head.x 0 || head.x colNum || head.y 0 || head.y rowNum } /** 判斷某個坐標是否與蛇身重合 */ isOnSnake(x: number, y: number): boolean { return this.body.some(segment segment.x x segment.y y) } /** 重置蛇狀態(tài)重新開局用 */ reset(): void { const gridX CONFIG.canvasWidth / CONFIG.gridSize const gridY CONFIG.canvasHeight / CONFIG.gridSize const startX Math.floor(gridX / 2) const startY Math.floor(gridY / 2) this.body [] for (let i 0; i CONFIG.snakeInitLength; i) { this.body.push({ x: startX - i, y: startY }) } this.direction Direction.Right this.pendingDirection Direction.Right this.movedThisTurn false } /** 繪制蛇 */ draw(ctx: CanvasRenderingContext2D): void { const size CONFIG.gridSize this.body.forEach((segment, index) { const px segment.x * size const py segment.y * size // 是否蛇頭 const isHead index 0 // 內(nèi)邊距讓蛇身看起來是一節(jié)一節(jié)的 const padding isHead ? 1 : 3 ctx.fillStyle isHead ? CONFIG.colorSnakeHead : CONFIG.colorSnakeBody // 用圓角矩形或正方形繪制蛇身 ctx.fillRect(px padding, py padding, size - padding * 2, size - padding * 2) // 給蛇頭畫出兩個眼睛增強辨識度 if (isHead) { ctx.fillStyle #fff const eyeSize 3 if (this.direction Direction.Right) { ctx.fillRect(px size - 8, py 4, eyeSize, eyeSize) ctx.fillRect(px size - 8, py size - 8, eyeSize, eyeSize) } else if (this.direction Direction.Left) { ctx.fillRect(px 5, py 4, eyeSize, eyeSize) ctx.fillRect(px 5, py size - 8, eyeSize, eyeSize) } else if (this.direction Direction.Up) { ctx.fillRect(px 4, py 5, eyeSize, eyeSize) ctx.fillRect(px size - 8, py 5, eyeSize, eyeSize) } else { // Down ctx.fillRect(px 4, py size - 8, eyeSize, eyeSize) ctx.fillRect(px size - 8, py size - 8, eyeSize, eyeSize) } } }) } }3.3 移動邏輯中的關(guān)鍵設(shè)計我在這里用了movedThisTurn這個標志位它在游戲循環(huán)里承擔(dān)著重要的作用。requestAnimationFrame 是每幀都會回調(diào)的但貪吃蛇不需要每幀都移動——它應(yīng)該按照snakeSpeed的節(jié)奏來走。于是我們只會每隔一段時間調(diào)用一次move()。但在一次移動完成之后如果用戶在這一幀里連續(xù)按了兩次方向鍵第一次的轉(zhuǎn)向已經(jīng)生效第二次的轉(zhuǎn)向只能進入 pendingDirection 緩沖區(qū)等下一輪移動時再消費。這個機制看起來簡單實際解決了兩個大問題。第一個問題是反向瞬移蛇頭正在向右走玩家在下一次移動前按了上又按了左如果沒有緩沖左方向會覆蓋上方向而左恰好是右的反方向蛇會瞬間穿過自己的身體。第二個問題是輸入丟失如果只用一個 direction而且每次鍵盤事件都直接覆蓋 direction那么玩家在短時間內(nèi)按的上和左只有最后一次生效看起來像是按鍵失靈。有了 pendingDirection按鍵就變成了隊列的感覺雖然只緩沖一個但手感已經(jīng)完全不同。還有就是move(grow: boolean)這個參數(shù)設(shè)計。grow 為 false 時移動完要把尾巴 pop 掉這樣蛇身長度保持不變。grow 為 true 時不 pop蛇身長度加一。這比吃到食物后再調(diào)一個專門的方法來加一節(jié)更符合直覺因為增長本來就應(yīng)該發(fā)生在移動這個瞬間。3.4 撞墻策略不穿墻還是穿墻我在move里做了一個邊界取模的兜底newHead.x (newHead.x colNum) % colNum newHead.y (newHead.y rowNum) % rowNum這個東西的意思是如果你的坐標到了 -1就把它修正成 colNum - 1如果到了 colNum就回到 0。也就是說蛇頭會從左邊穿出去、右邊鉆出來類似環(huán)屏效果。這在早期貪吃蛇里是一種常見玩法。但你可能想問既然蛇類里已經(jīng)做了邊界修正為什么還有一個isHitWall方法兩種玩法都存在經(jīng)典模式下蛇撞到邊界游戲結(jié)束穿墻模式則不然。把這個選擇交給控制器更合理——控制器知道當(dāng)前游戲該用哪種模式。在實際項目中我通常建議默認使用撞墻結(jié)束因為穿墻模式會讓玩家的策略變得完全不同難度把控也更含糊。在這個項目里我選擇在控制器里檢查撞墻結(jié)束游戲move里的取模只是防御性代碼確保極端情況下蛇頭始終在畫布內(nèi)。4. 游戲控制器類把一切串起來控制器類的名字我起的是 Game整個游戲的主循環(huán)、狀態(tài)管理、事件監(jiān)聽、Canvas 上下文管理都集中在它身上。它操作蛇和食物這兩個業(yè)務(wù)類但自身不關(guān)心蛇的數(shù)據(jù)結(jié)構(gòu)也不關(guān)心食物怎么畫它的工作是決策。4.1 游戲狀態(tài)機貪吃蛇看起來是實時游戲但邏輯上是一個狀態(tài)機至少包含這幾種狀態(tài)等待開始、運行中、暫停、結(jié)束。狀態(tài)不同事件處理和渲染行為完全不同。type GameStatus idle | running | paused | over export class Game { private canvas: HTMLCanvasElement private ctx: CanvasRenderingContext2D private snake: Snake private food: Food private status: GameStatus idle private score 0 private lastMoveTime 0 private animationId 0 constructor(canvas: HTMLCanvasElement) { this.canvas canvas // 這里在正式項目中應(yīng)該做空值處理但教學(xué)環(huán)節(jié)先保證存在 this.ctx canvas.getContext(2d)! this.snake new Snake() this.food new Food() this.food.generate() this.bindEvents() } }狀態(tài)機的好處在于任何外部輸入進來先看狀態(tài)再決定要不要響應(yīng)。比如游戲結(jié)束狀態(tài)下按方向鍵沒有任何意義暫停狀態(tài)下按空格應(yīng)該恢復(fù)運行中按空格應(yīng)該暫停。這個東西用 if 堆也能寫但狀態(tài)一多就會亂。我在項目里用狀態(tài)字符串加 switch 來處理。4.2 事件綁定與防止方向鍵滾動頁面鍵盤事件綁在 document 上而不是 canvas 上。為什么因為玩家按鍵盤時焦點不一定在 canvas 上如果綁在 canvas 上很可能出現(xiàn)按了沒反應(yīng)。而綁 document 能保證無論焦點在哪都能響應(yīng)。方向鍵的默認行為會讓頁面滾動必須preventDefault()。這是新手最容易忽略的問題不處理的話會出現(xiàn)按一下方向鍵蛇沒動頁面先滾了。private bindEvents(): void { document.addEventListener(keydown, (e) { const keyMap: Recordstring, Direction { ArrowUp: Direction.Up, ArrowDown: Direction.Down, ArrowLeft: Direction.Left, ArrowRight: Direction.Right, w: Direction.Up, s: Direction.Down, a: Direction.Left, d: Direction.Right } const dir keyMap[e.key] if (dir) { // 防止方向鍵滾動頁面 e.preventDefault() this.handleDirection(dir) } if (e.key || e.key Space) { e.preventDefault() this.handleSpace() } if (e.key Enter) { e.preventDefault() this.handleEnter() } }) }同時支持字母鍵和方向鍵這個體驗細節(jié)很加分。很多玩家習(xí)慣左手控制 WASD右手可能還握著鼠標。如果你只做了方向鍵會損失一部分玩家的舒適度。4.3 按鍵響應(yīng)與狀態(tài)流轉(zhuǎn)private handleDirection(dir: Direction): void { if (this.status ! running) return this.snake.setDirection(dir) } private handleSpace(): void { if (this.status running) { this.status paused } else if (this.status paused) { this.status running // 恢復(fù)時重置計時避免瞬間移動一次 this.lastMoveTime performance.now() } } private handleEnter(): void { if (this.status idle || this.status over) { this.start() } }這里有一個細節(jié)暫?;謴?fù)后lastMoveTime要重置為當(dāng)前時間。如果不重置暫停期間過去的時間會計入移動間隔恢復(fù)后瞬間就走一步玩家會感覺蛇跳了一下。類似的坑還有瀏覽器標簽頁切換回來后的大間隔問題也靠這個辦法解決。4.4 游戲循環(huán)用 requestAnimationFrame 加時間差控制游戲循環(huán)是控制器的靈魂。我用 requestAnimationFrame 驅(qū)動渲染但移動的節(jié)拍單獨用時間差控制。private loop (timestamp: number): void { // 記錄動畫幀ID便于取消 this.animationId requestAnimationFrame(this.loop) if (this.status ! running) { // 不管暫停還是結(jié)束畫面不動但為了方便顯示暫停文字還是重繪一次 this.draw() return } // 距離上次移動是否達到設(shè)定間隔 if (timestamp - this.lastMoveTime CONFIG.snakeSpeed) { this.update() this.lastMoveTime timestamp } this.draw() }為什么不直接setInterval來控制蛇移動誠實地講setInterval在這個場景其實也能用但有個一致性問題setInterval的回調(diào)可能會因為主線程忙而出現(xiàn)積壓導(dǎo)致多次快速連續(xù)執(zhí)行而且它和渲染不同步可能出現(xiàn)蛇移動了畫面卻沒更新。requestAnimationFrame 天然和瀏覽器渲染幀率同步在 60Hz 的屏幕上是 16.67ms 觸發(fā)一次移動節(jié)奏通過時間差來卡穩(wěn)定且不掉幀。loop定義為箭頭函數(shù)的原因是requestAnimationFrame(this.loop)里的 this 上下文如果換成一個普通函數(shù)在回調(diào)執(zhí)行時 this 會指向 window。箭頭函數(shù)則自動綁定當(dāng)前實例。4.5 更新邏輯碰撞檢測的先后順序update()是整個單幀決策的集中地。我處理碰撞的順序是先吃食物再撞自己再撞墻。private update(): void { const head this.snake.body[0] const foodPos this.food.position // 吃了食物嗎 const isEating head.x foodPos.x head.y foodPos.y // 讓蛇移動吃到食物就增長 this.snake.move(isEating) if (isEating) { this.score 10 this.food.generate(this.snake.body) } if (this.snake.isHitSelf()) { this.over() return } if (this.snake.isHitWall()) { this.over() return } }先判斷吃再調(diào)用 move。這里有個潛在的問題如果先調(diào)用 move 再判斷吃此時蛇頭已經(jīng)移動了就必須用移動前的坐標做判斷邏輯上更繞。先判斷再移動代碼讀起來順暢得多。isHitWall之所以放在isHitSelf之后是因為如果蛇頭和墻壁重合時同時也和自己的身體重合報哪個錯都合理。但實際游戲里蛇頭撞墻后不再移動此時蛇身不可能超過邊界所以先后順序影響不大保持這個順序只是因為讀代碼時先近后遠更自然。4.6 開始、結(jié)束、重繪private start(): void { this.score 0 this.snake.reset() this.food.generate(this.snake.body) this.status running this.lastMoveTime performance.now() } private over(): void { this.status over } private draw(): void { const { ctx } this ctx.clearRect(0, 0, CONFIG.canvasWidth, CONFIG.canvasHeight) ctx.fillStyle CONFIG.colorBg ctx.fillRect(0, 0, CONFIG.canvasWidth, CONFIG.canvasHeight) this.food.draw(ctx) this.snake.draw(ctx) // 游戲狀態(tài)文字 if (this.status idle) { this.drawCenterText(按 Enter 開始游戲) } else if (this.status paused) { this.drawCenterText(暫停中按空格繼續(xù)) } else if (this.status over) { this.drawCenterText(游戲結(jié)束得分 ${this.score}按 Enter 重來) } } private drawCenterText(text: string): void { const { ctx } this ctx.save() ctx.fillStyle #333 ctx.font bold 24px sans-serif ctx.textAlign center ctx.textBaseline middle ctx.fillText(text, CONFIG.canvasWidth / 2, CONFIG.canvasHeight / 2) ctx.restore() }draw 里的一個細節(jié)是 clearRect 之后用背景色鋪底。如果不鋪底背景默認是透明的看起來還行但如果你在 draw 里用了部分透明度的顏色比如半透明的蛇身下一幀繪制時會和上一幀殘留疊加形成拖影。先鋪一層不透明底色能徹底避免這個視覺錯誤。還有一個容易忽略的點drawCenterText 里的ctx.save()和ctx.restore()。save 保存當(dāng)前的繪圖狀態(tài)到棧里restore 恢復(fù)。這里之所以必須用是因為我改了textAlign和textBaseline。如果不恢復(fù)下一次繪制幾何圖形時這些文字對齊屬性會一直生效可能導(dǎo)致后續(xù)繪制出現(xiàn)莫名其妙的結(jié)果。但凡你在繪制過程中改了 Canvas 的全局狀態(tài)畫完就恢復(fù)是一個好習(xí)慣。5. 常見問題與排查技巧實錄寫貪吃蛇的過程當(dāng)中有幾類問題出現(xiàn)頻率極高。我把自己調(diào)試過程中踩過的坑整理一下按癥狀、原因、解法的方式列出來方便你對照排查。5.1 癥狀一蛇身顯示方向相反現(xiàn)象是蛇頭在最右但身體往右排看起來蛇在倒著走。排查后發(fā)現(xiàn)是初始化時 push 的順序?qū)е碌摹H绻愕拇a寫成this.body.push({ x: startX i, y: startY })i 從 0 遞增那么第 0 項是 startX第 1 項是 startX 1第 2 項是 startX 2以此類推。但我們的設(shè)計是 body[0] 永遠是蛇頭而蛇向右走時頭必須在最右邊。所以 body[0] 的 x 必須最大。正確的寫法是this.body.push({ x: startX - i, y: startY })這樣第 0 項 x 最大向右走時頭部插入的新位置永遠比原來大 1視覺和邏輯都是正確的。這個坑本質(zhì)上是數(shù)組第 0 項是蛇頭這個約定沒有貫徹到底。我建議在代碼注釋里寫上第 0 項為蛇頭以后自己看代碼也不會犯迷糊。5.2 癥狀二快速連按方向鍵導(dǎo)致蛇反向瞬移假設(shè)蛇向右移動玩家按了一下上緊接著按了一下左。如果沒有 pendingDirection 緩沖第二次按鍵后 direction 變成左但蛇頭還在向右移動下一次 move 時 head.x - 1 就會讓蛇頭直接往左而蛇身第二節(jié)還在原來的位置蛇頭就直接撞進了第二節(jié)的位置游戲可能瞬間判負。玩家會覺得莫名其妙。解法就是我在 Snake 類里實現(xiàn)的 setDirection 加 pendingDirection 機制。在設(shè)置方向時檢查新方向是否為當(dāng)前方向的反向或同向如果是就直接丟棄。然后再利用 movedThisTurn 做緩沖只允許方向變化一次多余的進入末位等待。這是最穩(wěn)妥的做法。5.3 癥狀三方向鍵按下時頁面跟著滾動如果你沒在 keydown 里調(diào) preventDefault方向鍵默認會讓瀏覽器滾動頁面尤其是你沒有給頁面設(shè)置 overflow hidden 的時候。用戶按下方向鍵游戲畫面沒反應(yīng)滾動條倒是動了。解決方式是 keydown 回調(diào)里對方向鍵統(tǒng)一調(diào)用 e.preventDefault()這在 bindEvents里已經(jīng)處理了。5.4 癥狀四游戲結(jié)束后蛇還在畫布上畫面有殘留游戲結(jié)束后 status 變成 overloop 里走的是 draw 分支draw 會先清空畫布再重繪食物和蛇所以畫面理論上不會殘留舊幀。如果你發(fā)現(xiàn)殘留多半是清屏寫錯了比如ctx.clearRect(0, 0, 100, 100)clearRect 的區(qū)域小于畫布實際尺寸導(dǎo)致邊緣沒清掉。用 CONFIG.canvasWidth 和 CONFIG.canvasHeight 代替魔法數(shù)字可以解決。5.5 癥狀五食物生成到蛇身上食物重新生成時應(yīng)該把蛇身的所有坐標傳給 generate 方法作為禁止區(qū)域。留意這個調(diào)用時機應(yīng)該在蛇移動并且長大完成之后調(diào)用否則傳的蛇身還是舊的缺少剛長出來的那一節(jié)。這個順序我一般在 update 里固定為move - 分數(shù) - 重新生成食物。5.6 癥狀六requestAnimationFrame 沒有啟動或者被瀏覽器節(jié)流瀏覽器為了省電會對后臺標簽頁的 requestAnimationFrame 做節(jié)流長時間掛后臺可能完全停止回調(diào)。這會導(dǎo)致什么你切出去一分鐘再切回來發(fā)現(xiàn)蛇沒有按照時間戳累計跳躍移動而是重新在這個位置繼續(xù)走。這不是 bug而是瀏覽器行為。如果你希望切回來時游戲立即補上這一分鐘的移動可以在 loop 里檢測 timestamp - lastMoveTime 如果大于某個閾值就只更新一次避免蛇一步跨出十萬八千里。我試過這個方案效果不錯帶來的問題是游戲難度會在切回來瞬間拔高所以也可以選擇接受瀏覽器節(jié)流不做額外處理。5.7 關(guān)于獲取 canvas 上下文時的感嘆號代碼里出現(xiàn)了這樣的寫法this.ctx canvas.getContext(2d)!TypeScript 里getContext的返回值類型是CanvasRenderingContext2D | null。當(dāng)你非常確定 canvas 元素存在且能拿到 2d 上下文時可以用非空斷言!來消除 null。但如果 canvas 的 id 寫錯了或者 canvas 元素還沒渲染到 DOM 就拿它這里會拋出運行時錯誤而不是編譯錯誤。比較穩(wěn)妥的寫法是判空后拋出明確錯誤const ctx canvas.getContext(2d) if (!ctx) throw new Error(無法獲取 canvas 上下文)這個寫法更安全錯誤信息也更明確。教學(xué)項目里直接用非空斷言省事但心里要清楚它到底在做什么別在團隊項目里隨手用感嘆號。5.8 關(guān)于分數(shù)和速度的平衡最后聊一個很多人做完游戲后都會問的問題怎么讓游戲越來越難最常用的方法是分數(shù)越高速度越快。我之前試過在 update 里動態(tài)計算const BASE_SCORE 10 const EXTRA_SCORE this.score * 0.5 const LIMIT_SPEED 60 // 最快間隔毫秒 CONFIG.snakeSpeed Math.max(LIMIT_SPEED, BASE_SPEED - EXTRA_SCORE)但這么做有個副作用蛇速變快后玩家的反應(yīng)時間變短如果速度增長過于陡峭游戲很快就變得不可玩。建議每次只減少 5 到 10 毫秒并且設(shè)置一個下限比如 60 毫秒保證游戲的可行性。具體閾值建議你在真機上調(diào)因為不同人的反應(yīng)速度和顯示器刷新率會有差異。我自己的實測經(jīng)驗60 毫秒是最快的極限再快人眼已經(jīng)很難判斷蛇頭位置了。80 毫秒到 100 毫秒是高手還能玩普通玩家勉強跟得上的甜點區(qū)。你也可以用累加得分的方式來分段提速比如每吃 5 個食物提升一檔速度這樣玩家有明顯的節(jié)奏感而不是每吃一個就變快一點那種體驗反而很累。還有一個值得嘗試的擴展方向食物得分可以多樣化。給普通食物 10 分偶爾出現(xiàn)一個特殊食物 50 分限時出現(xiàn)幾秒后消失。這會迫使玩家在追高分的風(fēng)險和穩(wěn)定推進之間做取舍。當(dāng)然這是進階玩法了基礎(chǔ)版先把狀態(tài)機跑通再往上疊加復(fù)雜度。蛇向前的方向判斷我最后再強調(diào)一遍在 move 里方向的計算用 this.direction而不是 this.pendingDirection。this.pendingDirection 只是緩沖真正的移動方向是在每次移動完成時從 pendingDirection 同步過來的。如果搞反了會出現(xiàn)按一下方向鍵蛇立刻轉(zhuǎn)向但畫面還沒移動視覺上非常割裂。這個順序我在調(diào)試時栽過跟頭所以拿出來單獨提醒你。代碼寫到這里貪吃蛇的核心類就全部完成了。你可以打開瀏覽器按 Enter 開始用方向鍵或 WASD 控制空格暫停死了按 Enter 重來。整個過程跑通之后再回頭去讀 snake.ts 和 game.ts 這兩個文件會發(fā)現(xiàn)所有的復(fù)雜性都被類的邊界切成了小塊。這個感覺就是面向?qū)ο笤O(shè)計在實戰(zhàn)項目中最直接的回報。
返回列表
PREV
查看更多資訊
NEXT
返回資訊列表
国产精品五月天婷婷| www.五月天婷婷姐姐| 在线日韩视频| 欧美色图45678| Se.婷婷五月天| 久色中文| 超碰91人人操| 爱超碰性| 手机旧版看人妻1025| 久久色区| 激情婷婷视频在线| 亚洲视频a| 成人色情五月天婷婷丁香| 五月天五月婷五月激情网| 婷婷丁香五月高清| 99色在线视频| 激情久久网| 久操人妻| 妇激情基地| 色欲香综合网| 色哟哟精品| 激情五月婷婷综合| 丁香六月成人网| 五月婷婷天堂| 台湾佬天天日丁香婷婷五月天| www夜夜操| 丁香五月天综合| 99综合五月免费视频色婷婷| 黑人熟妇一区二区三区| 伊人五月网| 开心激情播播五月天| 色综合色综合网| 99re欧美精品| 婷婷五月激情四月综合| 狠狠草狠狠草| 国产成人精品一区二三区熟女在线| 国产欧美性成人精品午夜| 色婷婷丁香社综合| 日本九九热| 亚洲色图五月丁香| 99热亚州综合| 五月天婷婷网站| 99re热在线观看| 欧美综合激情五月丁香| 97操视频| 免费在线观看AV网站| 久久精品综合色| 婷婷丁香五月精品| 91丨九色丨东北熟女| 激情五月婷婷视频| 玖玖伦理电影| 中文字幕AV在线播放| 成人综合视频网址| 日本激情ⅩXX免费视频| 五月色丁香| 天天狠狠综合精区| 亚洲 精品 综合 精品| 色欲一区二区三区精品A片| 五月丁香啪啪网| 99九精品| 伊人久久五月天综合| A久久| 婷婷激情综合色五月久久图片| 丁香五月色情| 色五月婷婷综合| 91麻豆国产三级精品福利在线观看| AV动漫不卡无码免费| 九九热在线视频,| 久久色9| 日日噜噜夜夜狠狠久久丁香五月| 欧美婷婷五月无砖| 五月天婷婷久久| 一级黄色影片| 国产成人亚洲综合亚洲| 欧美成人精品一区二区 | 99久精品视频| 色婷婷小说网| 五月天堂六月丁香亚州中文字幕久久| 丁香五月天激情AV| 国产精品第一国产精品| 中文字幕婷婷| 五月伊人网| 69人人操人人爽| 色播五月丁香综合| 亚洲欧洲中文日韩久久AV乱码| 色99日韩| 五月丁香婷婷久久| 丁香激情久久| 久久综合激情| 99热碰碰热| 深爱激情综合网| 丁香五月婷婷啪啪视频| 亚洲99在线| 激情第四色| 国产精品成人AV在线| 九九热在线观看视频| 色婷婷亚洲婷婷| 熟女啪啪视频| 久久伦乱| 伦99热| 五月亚洲| AV中文字幕夜夜操b天天摸bb| 亚洲综合网区| 丁香六月婷婷久久综合| 日韩综合成人| 激情综合网 激情五月天| 伊人综合网站| 天天综合永久| 九九综合网| 狠狠五月天婷婷激情网。| 丁香五月激情综合| 色五月激情综合| 久久激情五月天| 久久久99日本大片| 久久婷婷啪啪视频| 天天开心婷婷丁香五月| 9色91视频| 婷婷五月天影视| AA丁香综合激情| 日本的α片xxxwww| 日逼影音先锋男人AV资源站| 99九九精品视频| 天堂呦 呦百度搜索-百度搜索| 深爱五月婷婷开心中文字幕| 刘玥av在线| 一区色色色色网| 婷婷伊人綜合中文字幕| 婷婷爱综合| 亚洲日韩一页精品发布| 玖玖资源天天无码| 丁香五月婷婷天堂大香蕉| 99精品免费视频| 日韩 mm 不卡| 中文字幕在线播放视频| 婷婷色无码| 中文字幕 码精品视频网站| 天天拍夜夜撸| 丁香五月婷婷国产在线| 五月天激情四射网站| 色五月天丁香婷婷| 91五月天| 国产精品色婷婷AV综合色色| xxxx久| a在线观看| 人人亚洲| 久草五月丁香婷婷综合| 婷婷狠狠干| 色噜噜五月天| 国产精品久久久久久久久久| 亚洲色色香蕉| 国产精品成人在线| 精品国产AV色一区二区深夜久久 | 久久99这里只有精品视频| 无遮挡国产高潮视频免费观看| 亚洲、热| 日本 色综合| 久久多色| 中文无码精品一区二区三区| www.第四色99| 五月婷婷色影院| 开心久久xxx色| 美国少妇性做爰| 九九性视频| 亚洲旡码| 亚洲狠狠色丁香婷婷综合久久| 亚洲欧美国产高清vA在线播放| 变天就操逼婷婷五月| 噜噜操操| 五月丁香六月婷精品视频| 在线超碰精品| 99热只有| 天天骑天天操| 玖玖99福利| 99在线精品视频| 99久久婷婷五月天| 91精品无码| 国产精品天天狠天天看| 欧美性爱五月天| www.粉嫩av.com| 99色免费观看全部| 色一情一乱一乱一区91Av| 丁香五月激情综合在线观看| 欧美三级黄色片久久| AAA久久| 成人婷婷五月天| 五月激情四射网站| 婷婷免费视频| 久久开心五月天激情| 久久九九网| 一本久久婷婷| 91九色精品女同系列| 五月激情网五月综合网| 色婷婷888| 亚洲色无码A片一区二区麻豆| 狠狠色五月天| av人人操| 国产xxxxx在线观看| 日韩无码亚欧无码| 久久五月天综合| www.日日夜夜.com| 亚洲色婷婷网站| 91色吧网| 99热10在线高清播放| 丁香婷婷网| 激情开心五月亚洲| 色婷婷成人丁香| 美女91一起草| 丁香五月激情久久麻豆| 亚洲色99| 五月涩涩网| av九九| 另类图片五月激情| 这里只有精品免费视频| 蜜乳中文字| www.zbzhongsen.com| 婷婷黄色网| 99免费视频| 激情五月婷| 亚洲综合色丁香五月天| 亚洲小视频免费看| 五月婷精品| 婷婷六月久久| 另类婷婷五月天啪帕帕| 丰满少妇猛烈A片免费看观看| 久久性刺激| 久久机热这里只有精品| 综合激情五月丁香| 精品五月丁香| 99 热国产在| 亚洲有码在线视频| 婷婷色情五月| 操逼五月天| 日99网站| 免费做A爰片77777| 色墦五月丁香| 九九碰九九爱97| 青青草原99热| 婷婷六月天亚州| 色色网站在线免费观看视频| 激情综合五月婷| 婷婷五月天首页激情| 夜夜嗨一区二区三区直播内容| 天天操天天曰| 激情五月天 婷婷| 操逼视频一区| 五月天丁香成人| 超碰1999| 色婷婷成人| 丁香五月欧美色综合| 丁香婷婷丁香五月欧美人| www.com亚洲网站在线免费| 91狼友视频网页更新| 丁香五月激情综合| 夜夜爱网站| AV在线免费网站| 色色欧美色色| 日本社区五月天激情| 日逼免费视频| 六月婷婷日| 97超级碰| 综合久久十| 99在线视频操999| http:色情日本com| 日韩九区| 色播五月丁香| 9色91视频| 亚洲中文字幕在线观看| 五月成人丁香av91| av一区免费看| 九月婷婷综合网| 婷婷影院欧美| 久热丁香| www.超碰在线| 久婷婷五月丁香在线观看| 亚卅毛片| 色综合久| 91日在线视频| 九月婷婷人人操人人舔人人爱| 日韩国产在线精品| 色亭亭九月| 色婷婷色丁香色欲av| 婷婷综合天堂| 五月天狠狠色| 婷婷视频网| 国产日产成人亚洲欧美国产VA| 久久天天天| 九九激情网| 99色啊| 99热这里只有精品33| 五月婷婷色吧!| 五月婷婷综合潮喷| 狠狠操狠狠| 日韩在线视频中文字幕| 91欧美日韩综合| 玖玖精品视频| 国产午夜精品一区二区三区四区| 国产美女无遮挡裸体毛片A片| 成人片黄网站色大片免费毛片| 五月婷婷丁香日韩在线| 久久婷中文字幕| 91九色无码日韩| 日本色道视频网站| 九九成人精品| 丁香五月激情婷婷婷婷在线观看| 日良久久| 成人做爰高潮A片免费视频| 色色色色色色综合网| 国产一级黄色影片,| ady狠狠入| 婷婷丁香五月91| 337p大胆噜噜噜噜噜91Av| 日韩人妻无码一区二区| 五月天精品视频| 婷婷五月无码| 色色丁香| 亚洲激情四射| 国产操逼网站| 91久久| 六六久久黄色| 91九色精品| 思思久久99热| 蜜桃婷婷丁香五月天狠狠久久综合| 99超级碰碰| 人人干人人看| 五月天福利影院导航| 色爱综合网| 91热久久| 国产成人精品一区二三区熟女在线| 狠狠搞狠狠操| 色综合色色色色| 久久激情综合| 五月天激情啪啪| 天天激情5月天亚洲| 日韩成人综合| 2021日韩无码| 久九色| jiujiu热在线视频| 五月天狠狠干| 久碰婷婷视频| 另类天堂| 97久久超级| 九月婷婷激情| 婷婷五月永远18免费久久久| 九九这里都是精品| 日韩九区| 伊人玖玖网| 日韩 欧美 国产 一区 二区| 五月天婷婷乱论小说| 99成人在线观看| 色呦呦在线| 色婷网| 婷婷色五月情| 涩涩涩五月天| 欧美激情综合五月色丁香| 中文字幕操比影片| 丁香九月综合| 思思热在线精品视频| 亚洲久久激情| 久久一伦| 插插插色综合网| 五月花丁香婷婷| 婷婷综合色图| 久超免费视频| 五月婷婷五月丁香综合| 五月丁香六月婷婷色情| 国产精品18久久久| 91丨九色丨高潮丰满日本| 色青青五月| 色五月婷婷大香蕉| 1024AV视频| 色色五月天激情| 激情色视频| 久久99精品久久久久久三级| 亚洲A片成人无码久久精品青桔| 九色激情网| 五月丁香婷婷激情久久| 亚洲日日日| 日本丰满久久| 五月婷综合性中心| 欧美色婷婷| 日韩成人电影Av| 久久性爱视频| 日韩精品一品二区三区的使用体验| 无码少妇高潮喷水A片免费| 97超碰,人人舔,人人操,人人摸| 五月婷婷久久大香蕉| 一本伊人色婷| 欧类av怡春院| www.主妇. com| 激情丁香五月| 五月婷婷性爱视频| 五月婷婷激情69| www.五月天婷婷| 欧美日韩国产成人在线| 五月综亚洲| 久久五月网| 天天摸天天爽| 色婷婷小说| 97色色色色色色色| 五月天开心网| 丁香六月狠狠| 色婷婷激情| 亚洲国产精品成人免费一区久久久在线观看AAAA | 1024在线观看免费视频| www.henhengan| 久久五月婷综合| 亚洲欧洲自拍图片专区五月天| AAA久久| 男女99免费视频| 婷婷月综合| 任你擦免费视频| 五月天激情综合| 人人干人人操外国| 99热丁香| 大香蕉五月天婷婷| 欧美精品999| 丁香午月AV中文字幕| 99久久6| 激情五月天婷婷直播| 亚洲无码另类| 九九爱这里只有精品| 天天日天天舔| 日日干干天天干| 久久久一级AAA| 五月天婷婷青青草| 久久五月天免费网站| 天天爽天天摸天天爱| 色综合久久无码| 精品无吗va视频免费观看| 综合激情sV| 99精品在线观看视频| 国产91视频| 天天草天天爽| 大陆极品少妇内射AAAAAA| 婷婷综合欧美| 国产成人综合亚洲| 26uuu成人网| 婷婷欧美综合| 五月婷婷色| 亚洲综合色五月| Av在线不卡一区| 五月天伊人久久久久| 日韩在线视频中文字幕| 福利视频在线播放| 五月婷婷五月天激情网| 久月婷婷| 午夜无码熟熟妇丰满人妻| 国产精品香蕉| 伊人干综合| 色婷婷电影| 五月综合六月婷婷| 亚洲综合五月天婷婷丁香| 六月婷婷激情| 久久六月天| 玖玖综合色| 中文字幕无码人妻少妇免费视频| 97色色色色色| 天天五月丁香五月| YW无码| 亚洲无码播放| 秋霞影音91人妻久久| 六月丁香天堂| 国产欧美日韩综合精品一区二区| 成人va在线播放| 天天插天天插天天操| 六月五月天婷婷涩播在线| 五月丁香六月婷婷色情| 五月丁香六月婷婷综合免| 日本道久久91| 色情五月丁香| 久久玖玖综合| 这里只有九九精品| 国产67194| 天天肏视奸| 超碰成人免费| 狠狠操狠狠插| 男女久久婷婷五月天| 97碰| 99热www| 国产亚洲99久久精品| 人人草人| 亚洲综合视频一下| 99热在线观看免费中文| 欧美、日韩、中文、制服、人妻| 人妻第九页| 大香蕉网站,大香蕉综合| 婷婷激情欧美| 色色网站观看| 日本不卡中文字幕| 九九超碰人人| 色九九中文字幕| 日本天天操| 亚洲第一综合| 亚城区在线| 婷婷亚洲激情在线观看视频| 激情久久五月天| 色五月激情综合网站| 久草婷婷| 丁香五月激情棕合| 综合www色| 少妇伦子伦精品无吗| 色播婷婷大香蕉| 99热这里只有精品搜| 五月天色婷婷激情综合| 色啪综合| 日韩高清成人| 67194中文字幕| 色婷婷播放| 日韩AV免费电影在线播放| 丁香九月激情| 亚洲天堂爱爱| 91在线日| 色色色999| 超碰97免费在线| 99综合色| 99色色色色| 丁香五月在线视频黑人| 天天添天天摸天天天天做| 9九热视频| 香蕉婷婷| 99热情这里只有精品在线播放| 丁香婷婷五月色成人网站| 9久9久| 婷婷天天五月天| 久久密臀婷婷| 九月色婷婷婷| 丁香社92视频| 欧洲区自拍| 婷婷狠狠爱| 亚洲精品另类| 丁香五月婷婷激情完整版| 色停停影院五月天| 亚洲av无码精品色午夜| 婷婷99丁香| 99久久五月天| 爽极品色| 婷婷她六月天| 色五月久久成人婷婷| 日本的α片xxxwww| 久久这里只有精品网| 狠狠干总合| 爱的综合网| 99色热视频| 婷婷丁香激情五月天色色| 狠狠狠五月婷婷六月丁香| 色大综合| 狠狠人人婷婷| 影音先锋一区二区三区| 久久久这里有精品| 免费观看全黄做爰的视频| 七七婷婷综合| 中文在线视频久9| 五夜丁香| 日本综合色图| 国产精品18久久久| 婷婷五月综合中文字幕| 99超碰在线观看| 非洲一级AV| 玖玖在线视频福利| 精品人妻久久久久久久| 五月天狠狠| 97碰| 亚洲成av人影院| 五月婷婷很很色| 超爽内射| 九九成人精品免费视频| www.日日夜夜| 婷色五月天| 丁香九月久久| 免费无码毛片一区二区A片| 色综合网页| 台湾无码A片一区二区| 九九热只有这里精品| www.五月天婷婷姐姐| 色婷五月天激情| 在线视频另类| 五月婷婷爽爽爽| 日韩无码性爱| 第四色五月激情网| 色色色热热热| 五月天大香蕉| 99热久久日本| 天天拍夜夜撸| 婷婷五月天六月丁香| 六月婷婷色综合| 九九色婷婷五月天| 一逼色综合| 99精品在线下载| 超碰97干| 久婷婷视平| 91干视频| 丁香五月开心五月激情| 天天干天天干天天操| 色99久草在线| 久久最新色色色| 热99这就是精品视频| 九九热这里| 丁香五月婷婷久久久| 丁香五月婷婷久久综合激情网| 色综色五月天婷婷| 精品久久久人妻| 成人在线免费网址| 99爱欧美| 91超级碰碰| 丁香六月婷婷综合欧美| 五月丁香色综合| 日韩AV一区二区三区| 婷婷五月激情六月丁香| 超碰97干| 强奸幻女毛片| 夜夜骑日日夜夜| 婷婷丁香五另类网站| 免费视频1区| 久久视屏这里只有久久| 97色啪| 天堂在线伊久| 开心五月丁香综合久久| 国产成人网址| 亚洲AV第二区国产精品| 99成人免费热视频| 激情操逼婷婷| 六月婷婷无码观看| 日本色五月| av在线激情| 婷婷五月天在线综合| 日本精品人妻无码77777| WWW.99热| 婷婷综合色| 激情婷| 婷婷亚洲在线| 免费无码毛片一区二区A片| 大香蕉婷婷| 综合激情在线| 玖玖婷婷色| 欧美va在线| 婷婷丁香六月天| 丁香五月婷婷丫| 色色色色色网| 婷婷激情中文综合| 丁香六月啪啪| 九九黄色网| 色五月婷婷av| 五月天综合久久| WWW免费视频碰碰碰碰| 一本九九色| 丁香六月无码播放| 九色视频入口91| 丁香婷婷色五月天| 色色色宗合网| 五月丁香欧美综合| 色五月开心久久网| 国产午夜一区二区三区| 精品动漫 无码av| 色在线视频网2025| 久久精品99久久久久久久久| 夜夜躁爽日| 31色区视频免费看| 丁香五月天啪啪| 99啪在线| 久久伦乱| 久99热| 丁香五月激情网| 99视频地址| 五月婷婷在线网站| 欧洲电影在线观看免费版英语版| 婷婷五月天com| 综合久久婷婷99| 六月久久狠狠| 日韩免费99| 欧洲亚洲午夜| 色五月综合网| 精品一二三区久久AAA片| 五月丁香激情婷婷综合字幕| 欧美 色婷婷| 婷婷久久亚洲| 日韩精品成人在线| 亚洲这里只有精品| 色播五月丁香综合| 色色图五月天| 午夜成人天堂久久无码日韩久久| 丁香五月天AV在线| 欧美特大片黄| 五月亭亭狠狠| 99色激| 国外亚洲成AV人片在线观看| 六月丁香成人网| 久草丁香婷婷五月天婷| 婷婷丁香人妻天天爽| 综合网激情五月天| 国产在线视频1234| 激情五月丁香五月| 九九视频精品在线免费| 色 噜噜 九月 婷婷| 色哟哟www| 丁香六月狠狠| 丁香五月婷婷影视先锋| 久久综合婷婷| 天天天天做夜夜夜夜做| 99久re热| 天天射美女| 五月丁香色五月| 操操熟女| 亚洲99热| 99re视频精品| 日本综合色色| 搡BBBB搡BBB搡五十| 26uuu精品一区二区| 丁香婷婷六月天| 日韩黄色影院| 色色无码| 逼逼AV| 4438成人电影| 久久在线视频免费观看| 丁香婷婷基地| 婷婷五月综合欧美在线播放| 《丁香激情综合久久伊人久久》影视在线观看 -高清预告手机免费播放 -三妹影院 | 丁香五月色欲| 五夜婷婷| 午夜成人av在线| 四色永久成人网站| 亚洲噜色| 国产日批视频免费播放| 激情五月婷色| 婷婷亚洲综合| 五月丁香影视| 思思热在线精品视频网站| 色欧美影院| 亚洲中文字幕在线观看| 182TV大香蕉| 久久婷婷五月天激情| 色综合天堂| 97成人在线视频| 日韩成人网址| 99精品视频免费观看| 99久久a线观| 人人操人人操919999| 在线观看免费观看在线9久| 日熟女| 婷婷五月天97干| 99热97| 久久久区区一久久久久久| 色五月婷婷操逼| 久热精彩视频98| 狠狠操天天干| 九九操操| 色婷婷五月在线| 色色综合日韩| 色婷婷伊人| 久久aaaaa| 爱99干99| 亚洲成av人影院| 日韩啪啪视频| 超碰二区| 色综合色色色| 天天综合亚洲| 婷婷五月免费在线| 插插干干干色| 少妇丁香婷婷| 中文网AV| 亚洲精品无码久久| 九色91国产| 94干大香蕉| 婷婷五月娱乐在线| 婷婷丁香综合网| 国产成人精品一区二区三区视频| 天天激情站| 超碰人人在线| 综激情网| WWW.99热| 色综合中文| 六月丁香中文字幕| 99久久网站| 五月激情站| 色情五月停停丁香| 丁香久久| 六月丁香六月婷婷欧美| 亚洲va在线∨a天堂va欧美va| 五月婷六月天| 亚洲色婷婷视频| 激情婷婷另类| 伊人狠狠干| 色婷婷五月天堂资源| 开心五月丁香啪| 天天日人人爽| 五月开心深深爱激情综合| 婷婷五月天激情视频| 草婷婷在线| 99久久综合网| 丁香五月综合网亚洲综合欧美狠狠| 国产精品-91JQ就要激情网91JQ6.91JQ27.CASA:16888 | 91人人操人人看| 国产女人十八水真多1| 九九色婷| 玖玖综合玖玖| 超碰爱爱爱| 五月久熟女| 六月婷婷综合| 亚洲精品乱码久久久久99| 日韩 欧美 国产 一区 二区| 五月深爱婷婷| 丁香五月23111| rr天天操| 婷婷97碰碰| 亚洲激情五月天| 97婷婷色| 国产精品男人AV不卡| 亚洲电影中文字幕| 五月天社区婷婷丁香社区| 日韩 中文 欧美| 伊人网欧美在线男人天堂五月丁香| 婷婷五月深情丁香深爱日韩| 丁香婷婷五月天色播| 色噜综| 大婷婷色呦呦噜噜色呦呦噜噜| 丁香五月在线播放| 九九色综合九九色| 激情五月激情综合网| www,99热在线观看| 97操碰人人| 97色色-99久久| 深爱开心五月天| 婷婷成年人免费视频| 狠狠色五月| 久热91| 丁香六月激情综合网| 婷婷噜噜| 五月在线| 欧美激情综合| 69精品无码一区二区三区| 美女黄频aⅴ视频| 亚洲丁香五月天在线视频| 婷婷五月天资源| 久久综合爱| 免费看成人AA片无码视频吃奶| 丁香五夜激情四射夜夜夜| 亚洲乱码w在线观看| 色五月激情问网站| 国产午夜精品久久久观看| AV在线大香蕉| 五月丁香啪啪啪| 色99无码| 婷婷五月在线综合| 色无码| 色色com| 热久久婷婷| www.国产亚洲69ty.久久久久久久久久久久| 五月丁香色婷婷基地| 亚洲综合无码| 日本视频久久| 天天色图| 五月丁香狠狠| 丁香六月综合激情| 人人操碰| site:jszngf.com| 2025色婷婷| 成人无码髙潮喷水A片| 五月丁香久久色| 婷婷综合成人五月天| 99亚州综合精品成人网| 婷婷久久综合久| 亚洲精品视频电影| 开心五月婷婷激情网| 五月婷婷色色| 一区二区你懂的| 深爱五月综合网| 99热这里只有免费精品| www99热| 欧美精产国品一二三区| 五月天精品综合| 中文字幕在线日亚洲9| 98永久精品| 丁香六月亭亭久久综合| 99热在线播放| 日本97久久久精品| 人人爽亚洲| 国产乱人偷精品人妻A片| 99ri精品| 激情五月天影院| 亚洲AV久久久久久久久久久久久久久久 | 色婷婷av在线| 天天天久久人人人合| 欧美内射AAAAAAXXXXX| 欧美五月婷婷| 激情五月综合亚洲另类| 无码一区精品一区视频| 夜夜骑操AV| 99热网站| 91干视频| 中文久久婷婷| 亚洲成人无码片| 国产资源91在线| 日欧一片内射VA在线影院| 少妇丁香婷婷 | 丁香婷婷啪啪| 棕合影院色色| 亚洲成人无码网站| 激情五月婷婷在线观看| 欧美熟女视频 色婷婷| 激情五月综合ì香亚洲| 九九碰九九爱97| 97碰啪啪| 色综合久久综合中文综合网| 欧洲精品爱爱| 日韩精品一品二区三区的使用体验| 91美女被操| 九九热免费观看视频| 欧美天天五月丁香免费观看| 丁香五月AV| 国产欧美精品AAAAAA片| www,天天干| 妻久久久久| 五月婷婷黄色| 99久久这里只有精品免费官网| 99精品爱| 思思99热| 欧美在线视频免费播放| 天天噜日日噜综合无码| 狠狠色综合图片| 亚洲xx在线| 婷婷五月天激情五月天| www久久久久久久久久久久久久久久久| 婷婷激情五月综合基地| 亚洲另类电影| 五月色无码| 久久五月婷婷综合网| www超碰| 色婷婷影视| www.激情在线| 亚洲精品成人| 婷婷丁香人妻| 国产伦亲子伦亲子视频观看| 激情五月六月丁香| 色吧99| 99这里只有| WWW免费视频碰碰碰碰| 久久色五月天| 久久久人妻久久久| 成 人片 黄 色 大 片| 激情网第四色| 亚洲一区二区色图-亚洲精品国产精品乱码-成人AV | 2017狠狠干| 日本久久综合| 色色五月天婷婷| 青青热视频| 国产婷婷色综合AV蜜臀AV | 99秘 在线| 九九成年视频| 黄色成人网站在线播放| 午夜丁香| 国产精品美女久久久久AV超清| 精品成人无码A片观看香草视频 | 婷婷色综合| 综合在线网| 日韩精品一区二区三区,四区,五区视频| 深情六月婷婷综合久久| 666555。COm毛片| 丁香五月婷婷日本| 99在线观看视频| 日本精品人妻无码77777| 激情五月丁香综合网站| 激情 婷婷| www.婷婷五月天| 天天肏天天插| 99精品女人天堂| 99视频在线| 欧美Va在线| 99亚洲精品视频| 国产精品国产成人国产三级| 欧美性爱5月天天天看| av成人在线播放| 婷婷五月影院| 欧美成性色| 91a片爽| 色婷婷av在线观看| 婷婷六月色播| 黄色五月婷婷| 久久精品一区二区三区四区| 激情深爱五月天| 婷婷久久免费看| 日本专区久久| 色婷婷丁香五月天| 射区导航| 婷婷娱乐丁香综合网| 日本美女上人| 99在线资源视频| 久久久久亚洲AV成人无码电影| 狠狠干在线视频| 97人人做| www91色网站| 色欲婷婷五月天丁香| 激情综合激情五月| 99九九精品视频| 亞洲自怕| 26uuu精品一区二区| 天天干天天操天天拍| 同性gv国产精品一区二区| 五月婷婷,狠狠操| 丁香蜜臀黄色婷婷五月天| 天天日夜夜草进麻麻的子宫| 丁香六月久久| 91亚洲免费片| 五月丁香六月婷婷啪啪| 天天日,天天插| 激情五月丁香色婷婷| 色综合久久综合中文综合网| 我爱宗和色| 97精品综合久久内射| www.久久99| 五月丁香激情综合网| 色五月婷婷小说亚洲中文字幕组 | www.色婷婷| 99精品视频免费| renrencaoav| 99久久久国产大片区| 精品人妻伦一二三区久久| 国产精品国产| 五月婷婷丁香色播网| 天天免费成年人视频| 亚洲精品视频在线播放| 综合一啪| 懂色av粉嫩av蜜臀av| 亚洲婷婷五月天| 久久婷婷精品| 五月婷婷性| 99精品视频免费观看,| 激情小说五月天社区丁香| 成人Av在线大片| www久久久久久| 久久久婷婷五月天| 免费观看全黄做爰的视频| 99啪啪| 99久久久久| 99九九精品视频| 激情五月婷婷丁香| 华人在线免费| 高清无码中文字幕aVDV| 五月天色婷伊人| 亚洲av午夜精品一区二区| 内射 无码 伊人| 天堂在线观看视频| 国外亚洲成AV人片在线观看| 丁香婷婷激情| 色婷婷综合视频| 色婷婷av在线观看| 在线另类| 欧美日综合| 91五月天| 在线另类| 狠狠搞狠狠操| 97综合在线| 激情四射婷婷| 神马欧美精| 丁香五月网址| 偷偷操99| 九九99免费视频| 99精品久久久久久久婷婷| WWW99热| 久久色五月| 丁香六月啪| 亚洲激情五月天| 99热| 北京熟妇搡BBBB搡BBBB| 欧美性爱五月天| 年轻的妺妺伦理HD中文| 色色网站毛片| 国模九区| 丁香五月成人| 欧美肉大捧一进一出免费视频| 嫩BBB搡BBBB榛BBBB| 国产偷人爽久久久久久老妇APP| 国产精产国品一二三在观看 | 中文字幕在线不卡| 五月丁香777| 人人干av| 国产日产亚系列精品版优势| 91狠狠综合久久| 欧美日韩成人在线| 欧美色色色| 婷婷婷狠狠| 日本97人人| 久久伊人9| 婷婷五月综合免费在线| 久草网大香视频| 99久久精品免费精品国产_国产精品久久久久久_国产在线|日韩_久久国产精品电影 | 精品一二三区久久AAA片| 天天爽天天爽| 国产伦亲子伦亲子视频观看| 婷婷丁香色女人| 久久思思99| 91久久综合亚洲鲁鲁五月天| 久久66er久久| 五月天激情婷婷丁香| 五月丁香亚洲婷婷| 日韩精品999| 亚洲色五月婷婷| 99精品国产在热久久| 青青久久91| 91色综合网| 狠狠狠狠免费| 五月丁香婷婷六月| 99'无码| 六月婷婷五月丁香首页| 婷婷六月天亚州| 99热久久最新地址| 色综合色| 性爱综合网| 熟惀91九色在线| 久久人操-久草婷婷-成人AV| 日韩无码专区| 国产婷婷色五月| 色婷婷激情五月天在线观看| 爆乳熟妇一区二区三区爆乳照片| 乱码操操| 亚洲国产精品VA在线看黑人| 婷婷丁香五月在线观看91| 国产成人AV人人爽人人澡Va| 天天天天做夜夜夜夜做| 99爽视频| 免费无码毛片一区二区A片| 激情玖玖综合网| 大香蕉天堂| 五月天婷婷影院影院| 婷婷色五月婷| 大地9中文在线观看免费高清| 日韩婷久| 亚洲 综合中文| 久色资源网| 九九在线视频| 亚洲啪| 婷婷精品在线| 激情五月天色婷婷| 五月综合激情| 婷婷激情97| 成人羞羞啪啪 全 视频| 五月婷婷色五月| 六月色色| 久综合| 99色视频免费在线规看| 东京热免费视频| 丁香五月天激情四射网络不好| 激情五月天在线观看色婷婷| 草婷婷在线| 激情六月综合| 丁香五月激情综合婷综| 狠狠操天天操天天操| 欧美日韩成人在线网| 极品精品一区二区三区在线| 99操99| 激情五月婷婷综合| 国产XXXX搡XXXXX搡麻豆| 色五月婷婷影院| 99热这里只有国产精品| 26uuu亚洲| 日韩精品一品二区三区的使用体验| 久色视频首页| 婷婷五月天色| 午夜丁香 婷婷| 九九热这里都是精品6| 大香蕉久久久| 五月天伊人久久| 成年人99热| 思思99热| 亚洲在线综合| 五月婷婷激情网| WWW色五月| 综合色天天| 99热这里只有精品5| 成人电影AV在线观看| www.91有码.com| 欧美25p| 玖玖色综合网| 丁香六月久久| 狠狠色婷婷丁香五月| 久久机只有这里精品| 国产毛片精品一区二区色欲黄A片| 色婷久| 婷婷久久色| 国产91在线视频| 婷婷激情鹿城五月天| 婷婷五月天色色| 狠狠操之狠狠操| 国产99热| 久久东京热婷婷五月| 在线看片av| 色天堂A| 五月丁香色婷婷| 久七香蕉| 五月婷婷五月色| 婷婷丁香综合在线| 亚洲性爱区无码区| ZpRSw| 色色网五月激情| 激情AV综合| 五月婷婷九月婷婷九月婷婷| 亚洲狠狠终合停停终合| 狠狠色情婷婷| 国产亚洲精品久久久久久久久动漫| 久久与婷婷| 国产伦亲子伦亲子视频观看| 99在线免费视| 欧美私人家庭影院| 丁香五月婷婷激情123| 99热国产这里只有精品| 五月久久五月激情| 九九色逼| 97干婷婷五月天| 乱岳熟女50岁| 国产一区男女| 激情九月婷婷九月| 亚洲夜夜操| 亚洲视频在线观看区| 亭亭五月丁香五月天激情| 五月激情婷婷国产精品久久久久久| 久久久久人妻网址| 国产精品国产| 五月天婷婷色色网| 成人国产综合| 激情五月天婷婷| 另类五月婷婷| 秋霞av吧| 五月天激情小说欧美激情| 九九久久免费视频44| 婷婷六月丁香欧美视频在线| 色天堂操| 亚洲最大激情无码| 久久在这里99| www色婷婷| 色99综合视频| 91主播在线| 五月婷久久在线| 欧美色偷偷大香| 色色狼人综合| 亚洲小视频免费播放| 中文字幕丰满孑伦无码专区| 99免费视频网| 天堂色婷婷| 天天日天天添| 九九热精品| 天天舔天天插天天干| 丁香五月婷婷婷桃花影院| 99爱视频| 五月婷婷偷| 热婷婷av| 日本熟妇精品99| 丁香六月婷婷综合激情欧美| 久久婷婷五月丁香蜜桃网| 亚洲久久激情| 九九视频免费| 婷婷大香焦| 九九热精品99| 欧美天天五月丁香免费观看| 99天堂在线观看免费视频| 人人操av| 亚洲第一色色色色| 久久嘟嘟丁香| 色色激情| 丁香五月开心五月激情| 婷婷五月天六点丁香五月| 日日影院 | 国产精品国产| 欧美又粗又大一区二区在线观看| 中文超碰视在线| 99久久婷婷国产综合精品草原| 天天色综合综合| 五月婷丁香| 五月婷婷伊|