Stories

Detail Return Return

鴻蒙元服務實戰-笑笑五子棋(4) - Stories Detail

鴻蒙元服務實戰-笑笑五子棋(4)

我們在這一章節主要實現五子棋的基本邏輯

核心目錄結構

├─ets
│  ├─entryability
│  │      EntryAbility.ets
│  │
│  ├─entryformability
│  │      EntryFormAbility.ets
│  │
│  ├─pages
│  │      Index.ets
│  │
│  ├─views
│  │      About.ets
│  │      Home.ets
│  │
│  └─widget
│      └─pages
│              WidgetCard.ets
│
└─resources
    ├─base
    │  ├─element
    │  │      color.json
    │  │      float.json
    │  │      string.json
    │  │
    │  ├─media
    │  │      right.svg
    │  │      startIcon.png
    │  │
    │  └─profile
    │          form_config.json
    │          main_pages.json
    │
    ├─en_US
    │  └─element
    │          string.json
    │
    ├─rawfile
    └─zh_CN
        └─element
                string.json

沉浸式設計

image-20250105111010688

  1. entry/src/main/ets/entryability/EntryAbility.ets 中統一設置

      onWindowStageCreate(windowStage: window.WindowStage): void {
        windowStage.getMainWindow()
          .then(windowClass => {
            // 設置沉浸式
            windowClass.setWindowLayoutFullScreen(true)
    
            // 頂部狀態欄
            const topAvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
            const topRectHeight = topAvoidArea.topRect.height;
            // px轉vp
            const vpTopHeight = px2vp(topRectHeight)
    
            // 底部導航條
            const bottomAvoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
            const bottomRectHeight = bottomAvoidArea.bottomRect.height;
            const vpBottomHeight = px2vp(bottomRectHeight)
    
            AppStorage.setOrCreate('topRect', vpTopHeight)
            AppStorage.setOrCreate('bottomRect', vpBottomHeight)
          })
        windowStage.loadContent('pages/Index', (err) => {
          // ...
        });
      }
  2. 頁面通過 padding 避開頂部和底部

      @StorageProp("topRect")
      topRect: number = 0
      @StorageProp("bottomRect")
      bottomRect: number = 0
      build() {
        Column() {
        // ...
        }
        .width('100%')
        .height('100%')
        .linearGradient({
          colors: [["#DEF9ED", 0], ["#F4F5F7", 0.4]]
        })
        .padding({
          top: this.topRect,
          bottom: this.bottomRect
        })
      }

    image-20250105111545129

AtomicServiceTabs

AtomicServiceTabs是元服務獨有的 tab 組件。Tabs組件後續不再支持在元服務中進行使用。,對 Tabs 組件一些不需提供給用户自定義設計的屬性進行簡化,限制最多顯示 5 個頁籤,固定頁籤樣式,位置和大小。

image-20250105111747871

基本用法

AtomicServiceTabs({
  // 內容
  tabContents: [
    () => {
      // 自定義構建函數
      this.tabContent1();
    },
    () => {
      // 自定義構建函數
      this.tabContent2();
    },
  ],
  // 標題
  tabBarOptionsArray: [
    new TabBarOptions(
      $r("sys.media.save_button_picture"),
      "玩吧",
      "#666",
      "#07C160"
    ),
    new TabBarOptions($r("sys.media.AI_keyboard"), "關於", "#666", "#07C160"),
  ],
  // 標題顯示的位置
  tabBarPosition: TabBarPosition.BOTTOM,
  // 背景顏色
  barBackgroundColor: 0xffffff,
});

Home 和 About

image-20250105112201927

Home 表示首頁,用來顯示主要內容

About 表示關於,用來存放項目的基本信息

他們目前都是普通的組件,分別放在 tabContent1 和 tabContent2 內

引入 canvas

在 Home 中開始引入canvas

@Component
export struct Home {
  settings: RenderingContextSettings = new RenderingContextSettings(true);
  ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);

  build() {
    Column() {
      Canvas(this.ctx)
        .width(width)
        .height(width)
        .backgroundColor(Color.Orange)
    }
    .width("100%")
    .height("100%")
    .justifyContent(FlexAlign.Center)
  }
}

繪製棋盤

image-20250105145524700

繪製棋盤的思路如下:

  1. 確定要繪製多少個格子。
  2. 每一個格子多大

這裏的想法比較簡單:

  1. 確定要繪製的格子是 15 個。

    gridSize: number = 15;
  2. 每一個格多大,由屏幕寬度決定。比如屏幕寬度的 90%,然後分成 15 份。每一份就是格子的寬度

    // 獲取屏幕的寬度的 90%
    const width = px2vp(display.getDefaultDisplaySync().availableWidth) * 0.9;
    // 棋盤是正方形的,所以高度和寬度相等
    const height = width;
    
    cellSize: number = width / this.gridSize;
  3. 然後封裝描繪畫面的方法 drawBoard
// 繪製棋盤
drawBoard = () => {
  this.ctx.clearRect(0, 0, width, height);
  // 繪製網格
  this.ctx.strokeStyle = "#000";
  this.ctx.lineWidth = 1;
  for (let i = 0; i < this.gridSize; i++) {
    this.ctx.beginPath();
    this.ctx.moveTo(this.cellSize * i, 0);
    this.ctx.lineTo(this.cellSize * i, height);
    this.ctx.stroke();

    this.ctx.beginPath();
    this.ctx.moveTo(0, this.cellSize * i);
    this.ctx.lineTo(width, this.cellSize * i);
    this.ctx.stroke();
  }
};
  1. canvas 準備好的時候開始繪製

    Canvas(this.ctx)
      .width(width)
      .height(width)
      .backgroundColor(Color.Orange)
      .onReady(() => {
        this.drawBoard();
      });

點擊下棋

點擊下棋要是做挺多的處理的,比如:

  1. 當前是下黑棋還是白棋
  2. 下完這一子之後,勝利了還是繼續下。

我們開始吧:

  1. 初始化棋盤數據,它是一個二維數組,下棋的時候,其實也是往裏面填充內容

      // 棋盤數據
      board: number[][] = []
  2. 初始化當前下棋的角色 設定 1:黑旗 ,2:白旗

    currentPlayer: number = 1; // 當前玩家 (1: 黑子, 2: 白子)
  3. 聲明初始化棋盤的函數,負責初始化棋盤數據和描繪棋盤

      initGame = () => {
        this.board = []
        for (let index = 0; index < this.gridSize; index++) {
          const arr: number[] = []
          for (let index2 = 0; index2 < this.gridSize; index2++) {
            //  0 表示當前沒有人在下棋
            arr.push(0)
          }
          this.board.push(arr)
        }
        // this.currentPlayer = 1;
        // this.gameOver = false;
        // this.textContent = '輪到黑子落子';
        this.drawBoard();
      }
    
     -------------------
       Canvas(this.ctx)
        .width(width)
        .height(width)
        .backgroundColor(Color.Orange)
        .onReady(() => {
          this.initGame()
        })
  4. 聲明點擊棋盤事件,事件中執行下棋邏輯

    handleClick = async (event: ClickEvent) => {
      const x = event.x;
      const y = event.y;
    
      const col = Math.floor(x / this.cellSize);
      const row = Math.floor(y / this.cellSize);
    
      if (this.board[row] && this.board[row][col] === 0) {
        this.board[row][col] = this.currentPlayer;
        this.drawBoard();
        this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
      } else {
        promptAction.showToast({ message: `請點擊中棋盤對位位置` });
      }
    };
  5. 調整 drawBoard 函數,根據 this.board[row][col] 描繪出旗子

    // 繪製棋盤
    drawBoard = () => {
      this.ctx.clearRect(0, 0, width, height);
    
      // 繪製網格
      this.ctx.strokeStyle = "#000";
      this.ctx.lineWidth = 1;
      for (let i = 0; i < this.gridSize; i++) {
        this.ctx.beginPath();
        this.ctx.moveTo(this.cellSize * i, 0);
        this.ctx.lineTo(this.cellSize * i, height);
        this.ctx.stroke();
    
        this.ctx.beginPath();
        this.ctx.moveTo(0, this.cellSize * i);
        this.ctx.lineTo(width, this.cellSize * i);
        this.ctx.stroke();
      }
      // 繪製已落的棋子
      for (let row = 0; row < this.gridSize; row++) {
        for (let col = 0; col < this.gridSize; col++) {
          if (this.board[row][col] !== 0) {
            this.ctx.beginPath();
            this.ctx.arc(
              col * this.cellSize + this.cellSize / 2,
              row * this.cellSize + this.cellSize / 2,
              this.radius,
              0,
              2 * Math.PI
            );
            this.ctx.fillStyle = this.board[row][col] === 1 ? "black" : "white";
            this.ctx.fill();
            this.ctx.stroke();
          }
        }
      }
    };
  6. 效果

    PixPin_2025-01-05_15-20-57

判斷輸贏

五子棋判斷輸贏的方法比較簡單,只需要知道是否有五子連珠就行

  1. 定義判斷輸贏的方法 checkWin

        // 判斷是否有五子連珠
        checkWin = (row: number, col: number) => {
          // 定義一個接口abc,用於表示方向相關的偏移量,dr表示行方向的偏移量,dc表示列方向的偏移量
          interface abc {
            dr: number
            dc: number
          }
    
          // 定義一個包含四個方向偏移量信息的數組,分別對應不同的檢查方向
          const directions: abc[] = [
            { dr: 0, dc: 1 }, // 水平方向,行偏移量為0,列偏移量為1,即向右檢查
            { dr: 1, dc: 0 }, // 垂直方向,行偏移量為1,列偏移量為0,即向下檢查
            { dr: 1, dc: 1 }, // 主對角線方向,行和列偏移量都為1,向右下方向檢查
            { dr: 1, dc: -1 }// 副對角線方向,行偏移量為1,列偏移量為 -1,即向右上方向檢查
          ];
    
          // 遍歷四個不同的方向,依次檢查每個方向上是否有五子連珠情況
          for (let i = 0; i < directions.length; i++) {
            const dr = directions[i].dr;
            const dc = directions[i].dc;
            let count = 1;
    
            // 向一個方向檢查(從當前落子位置開始,沿着指定方向向前檢查)
            // 循環嘗試查找連續相同顏色的棋子,最多查找連續4個(因為已經有當前落子算1個了,湊夠5個判斷贏)
            for (let i = 1; i < 5; i++) {
              let r = row + dr * i;
              let c = col + dc * i;
              // 判斷當前位置是否在棋盤範圍內,並且此位置的棋子顏色是否和當前玩家的棋子顏色相同
              if (r >= 0 && r < this.gridSize && c >= 0 && c < this.gridSize && this.board[r][c] === this.currentPlayer) {
                count++;
              } else {
                break;
              }
            }
    
            // 向另一個方向檢查(從當前落子位置開始,沿着指定方向的反方向檢查)
            // 同樣循環嘗試查找連續相同顏色的棋子,最多查找連續4個
            for (let i = 1; i < 5; i++) {
              let r = row - dr * i;
              let c = col - dc * i;
              if (r >= 0 && r < this.gridSize && c >= 0 && c < this.gridSize && this.board[r][c] === this.currentPlayer) {
                count++;
              } else {
                break;
              }
            }
    
            // 如果在當前方向(正方向和反方向結合起來)上連續相同顏色的棋子數量達到或超過5個,則表示當前玩家勝利
            if (count >= 5) {
              return true;
            }
          }
    
          // 如果遍歷完所有方向都沒有出現五子連珠的情況,則返回false,表示當前落子未形成勝利局面
          return false;
        }
  2. 在點擊下棋時 判斷是否輸贏 handleClick

        if (this.board[row] && this.board[row][col] === 0) {
          this.board[row][col] = this.currentPlayer;
          this.drawBoard();
    
          if (this.checkWin(row, col)) {  // 執行後續邏輯
  3. handleClick中判斷輸贏後,再做後續的一些小邏輯

    1. 如 還沒決定輸贏,繼續下棋
    2. 決定輸贏了,彈出對話框恭喜勝利者, 詢問是否還要再下一盤。。
    3. 完整代碼
    // 處理玩家落子
    handleClick = async (event: ClickEvent) => {
      if (this.gameOver) {
        return;
      }
    
      const x = event.x;
      const y = event.y;
    
      const col = Math.floor(x / this.cellSize);
      const row = Math.floor(y / this.cellSize);
    
      if (this.board[row] && this.board[row][col] === 0) {
        this.board[row][col] = this.currentPlayer;
        this.drawBoard();
    
        if (this.checkWin(row, col)) {
          this.textContent =
            this.currentPlayer === 1 ? "黑子勝利!" : "白子勝利!";
          this.gameOver = true;
          // AlertDialog.show({ message: this.textContent })
          const res = await promptAction.showDialog({
            title: this.textContent,
            message: "重新再來一盤嗎",
            buttons: [
              { text: "不了", color: "#000" },
              { text: "來吧", color: "#0094ff" },
            ],
          });
          if (res.index === 1) {
            this.initGame();
          }
        } else {
          this.currentPlayer = this.currentPlayer === 1 ? 2 : 1;
          this.textContent =
            this.currentPlayer === 1 ? "輪到黑子落子" : "輪到白子落子";
        }
      } else {
        promptAction.showToast({ message: `請點擊中棋盤對位位置` });
      }
    };
  4. 效果

    PixPin_2025-01-05_15-36-24

總結

本章節多了一些業務的具體實現,尤其是下棋的一些邏輯處理上。

如果你興趣想要了解更多的鴻蒙應用開發細節和最新資訊,歡迎在評論區留言或者私信或者看我個人信息,可以加入技術交流羣。

user avatar jingmingdewudongmian_dscnyw Avatar finally-vince Avatar wodekouwei Avatar smallhuifei Avatar feichangkudexiang Avatar
Favorites 5 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.