Phần 3. Xây dựng Logic chính màn hình Game 2D

0

Tiếp nối series học lập trình react native thông qua tự viết một game 2D. Phần này, chúng ta sẽ viết code cho những logic chính của game.

Nếu bạn chưa hiểu chuyển gì xảy ra thì mời bạn đọc lại bài viết trước:

Khởi tạo màn hình game

Để quy hoạch mã nguồn nó clean, chúng ta sẽ tạo một thư mục game trong thư mục screens. Trong thư mục này, bạn hãy tạo 2 files: index.js và styles.js tương ứng với logic và định dạng giao diện của màn hình game này.

Để định nghĩa router cho màn hình game này, bạn mở Routers.js và import màn hình game:

import Game from "./Game";

và thêm vào stack của navigator (bạn có thể tham khảo cách đã làm với màn hinh home)

const StackNavigator = createStackNavigator(
 {
   Home: {
     screen: Home
   },
   Game: {
     screen: Game
   }
 },
…

Lúc này, nếu bạn lưu code và chạy thử sẽ gặp lỗi crash ngay, lý do vì Game/index.js đang chưa có nội dung gì cả. Nhưng không sao, chúng ta sẽ làm nó hết lỗi ngay bây giờ. Đơn giản nhất là cứ clone code từ màn hình Home sang đã.

import React, { Component } from "react";
import { View } from "react-native";
import { Header } from "../../components";
import styles from "./styles";

export default class Home extends Component {
 render() {
   return (
     <View style={styles.container}>
       <Header />
     </View>
   );
 }
}

Ok, thêm chút “mông má” cho nó trong styles.js

import { StyleSheet } from "react-native";

export default StyleSheet.create({
 container: {
   flex: 1,
   backgroundColor: "#0a0a0a",
   justifyContent: "center",
   alignItems: "center",
 }
});

Như bạn đã thấy, màn hình Game này chúng ta có tái sử dụng lại Header component. Tuy nhiên, chúng ta có thể cần phải sửa lại một chút vì font chữ nó hơi nhỏ.

[Phần 3] Xây dựng Logic chính màn hình Game 2D

Bạn có thể sử dụng thuộc tính fontSize để tăng giảm kích thước font chữ trong trường hợp này. Tuy nhiên, mình sẽ gợi ý cho bạn một cách khác, ưu việt hơn, giúp bạn dễ custom hơn trong tương lai.

PropTypes là gì?

Trong React, bạn sẽ thường xuyên bắt gặp khái niệm PropTypes, hiểu nôm na là cách bạn kiểm tra các props được truyền vào component thuộc kiểu dữ liệu gì.

Khi ai đó sử dụng component của bạn, họ sẽ biết được props đó là gì, kiểu dữ liệu truyền vào là gì và có bắt buộc phải truyền dữ liệu cho props đó hay không?

Ok, giờ bạn mở components/Header.js và thêm dòng:

Header.propTypes = {
 fontSize: PropTypes.number
}

Header.defaultProps = {
 fontSize: 55
}

Xóa thuộc tính fontSize trong StyleSheet để tránh bị conflict. Sau đó đó thay đổi tham số truyền vào của Header:

const Header = ({ fontSize }) => ( … }

Từ bây giờ, bạn có thể sử dụng fontSize cho tất cả các Text component, kiểu đại khái như sau:

<Text style={[styles.header, { fontSize }]}>blinder</Text>

Navigate từ một màn hình sang một màn hình khác

Cách để navigate giữa các màn hình phổ biến nhất là sử dụng thư viện react-navigator (thư viện do chính nhà phát hành react phát triển). Chúng ta chỉ cần gọi: this.props.navigation.navigate(‘Game’); là được.

Tuy nhiên, có một cần lưu ý là khi bạn đang ở màn hình Game, nếu bạn vuốt sang trái, mặc định nó sẽ quay về màn hình Home. Với trường hợp là ứng dụng bình thường thì nó đúng, “hợp tình hợp lý”. Nhưng với game thì lại khác, đặc biệt là bạn đang chơi game căng thẳng mà vô tình bị back về như thế lại toang. Mình sẽ giới thiệu một ý tưởng khá hay để tắt tính năng back này.

Lưu ý: Khi bạn đã tắt thanh navigation của hệ thống thì bạn cần phải thiết kế nút back trong game để người dùng có thể quay lại màn hình Home.

Để tắt tính năng vuốt, bạn mở Routers.js và thêm option sau:

Game: {
     screen: Game,
     navigationOptions: {
       gesturesEnabled: false,
     },
   }

Giới thiệu game play

Khi người dùng bắt đầu chơi game, họ sẽ thấy một lưới 2*2 như hình bên dưới:

mobile-game-grid

Bắt đầu game, người chơi có 0 điểm và 15 giây đếm ngược. Khi chạm vào ô màu đúng ( là ô màu khác với các ô màu còn lại), người chơi được cộng 1 điểm và thêm 3 giây. Cứ như vậy cho đến khi người chơi chán thì thôi, game này là một vòng lặp vô tận, có thể không bao giờ chiến thắng.

Lưới sẽ tăng dần từ 2*2 tới tối đa 5*5.

react-native-mobile-game-mechanics

Generate ngẫu nhiên màu RGB

Để mã nguồn được clean, chúng ta sẽ không viết đoạn code gen màu này trong thư mục Game, thay vào đó, bạn tạo thư mục utilities. Trong thư mục này thì tạo file index.js và color.js

Trong index.js, bạn thêm đoạn code sau:

export * from './color'

export default {}

Để gen mã màu ngẫu nhiên, bạn thêm đoạn code sau vào trong color.js

export const generateRGB = () => {
   const r = Math.floor(Math.random() * 255);
   const g = Math.floor(Math.random() * 255);
   const b = Math.floor(Math.random() * 255);
   return { r, g, b }
};

export const mutateRGB = ({ r, g, b }) => {
   const newR = r + Math.floor(Math.random() * 20) + 10;
   const newG = g + Math.floor(Math.random() * 20) + 10;
   const newB = b + Math.floor(Math.random() * 20) + 10;
   return { r: newR, g: newG, b: newB }
};

Bạn có thấy khó hiểu không? Nguyên lý cơ bản của nó là tạo ra một số ngẫu nhiên trong khoảng 10-20 và thêm nó vào giá trị RGB ban đầu, sau đó thì trả về mã màu RGB mới.

Thực hiện logic Màn hình Game

Đầu tiên, chúng ta khởi tạo giá trị mặc định khi vào game:

state = {
   points: 0,
   timeLeft: 15,
 };

Như mình đã giới thiệu ở phần game play, chúng ta sẽ điếm ngược thời gian cho đến khi về 0 (về 0 là người chơi bị thua).

componentWillMount() {
   this.interval = setInterval(() => {
     this.setState(state => ({ timeLeft: state.timeLeft - 1 }));
   }, 1000);
 }

 componentWillUnmount() {
   clearInterval(this.interval);
 }

Xây dựng grid

Mở file screens/Game/index.js và thêm đoạn code import sau:

import { generateRGB, mutateRGB } from '../../utilities';

Sau đó thì định nghĩa thêm state cho mã màu:

state = {
   points: 0,
   timeLeft: 15,
   rgb: generateRGB()
 };

Tiếp theo, định nghĩa thêm const trong hàm render() để về sau:

const { rgb } = this.state;
const { width } = Dimensions.get("window");

Và nội dung hàm render() như sau:

render() {
    const { rgb, size, diffTileIndex, diffTileColor } = this.state;
    const { height } = Dimensions.get("window");
    return (
      <View style={styles.container}>
        <Header />
        <View
          style={{
            height: height / 2.5,
            width: height / 2.5,
            flexDirection: "row"
          }}
        >
          {Array(size)
            .fill()
            .map((val, columnIndex) => (
              <View
                style={{ flex: 1, flexDirection: "column" }}
                key={columnIndex}
              >
                {Array(size)
                  .fill()
                  .map((val, rowIndex) => (
                    <TouchableOpacity
                      key={`${rowIndex}.${columnIndex}`}
                      style={{
                        flex: 1,
                        backgroundColor:
                        rowIndex == diffTileIndex[0] && columnIndex == diffTileIndex[1]
                            ? diffTileColor
                            : `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
                        margin: 2
                      }}
                      onPress={() => this.onTilePress(rowIndex, columnIndex)}
                    />
                  ))}
              </View>
            ))}
        </View>
      </View>
    );

Mình sẽ giải thích chi tiết một chút về đoạn mã trên: Chúng ta tạo một Array rỗng có kích thước là 2 (câu lệnh Array(2)). Tạo một <View> có style với flex: 1 để điền vào các ô trống. Và để đảm bảo các row nằm bên dưới, chúng ta sử dụng thuộc tính flexDirection: column

Sau đó, bên trong các row này, nó tạo các ô bằng cách tạo các mảng rỗng khác và làm giống như trên, thêm TouchableOpacity cho mỗi lần lặp.

Kết quả sẽ được như này:

mobile-game-screen

Như đoạn code, kích thước của grid đang được hardcode là 2*2, để có thể tăng kích thước grid lên 3*3,…, 5*5 thì sao? Đơn giản chúng ta thêm state size:

state = {
  points: 0,
  timeLeft: 15,
  rgb: generateRGB(),
  size: 2
};

Sau đó, thay thế những chỗ hard code Array(2) bằng Array(size).

Tiếp theo, chúng ta sẽ gen ra một ô có màu khác biệt:

generateSizeIndex = size => {
 return Math.floor(Math.random() * size);
};

generateNewRound = () => {
 const RGB = generateRGB();
 const mRGB = mutateRGB(RGB);
 const { points } = this.state;
 const size = Math.min(Math.max(Math.floor(Math.sqrt(points)), 2), 5);
 this.setState({
   size,
   diffTileIndex: [this.generateSizeIndex(size), this.generateSizeIndex(size)],
   diffTileColor: `rgb(${mRGB.r}, ${mRGB.g}, ${mRGB.b})`,
   rgb: RGB
 });
};

Xử lý việc tap vào các ô màu

Phần này, chúng ta sẽ xử lý việc cộng điểm và thêm thời gian khi người chơi chọn đúng ô, đồng thời tạo mầu mới và tăng kích thước grid nếu cần. Ngược lại, trừ thời gian nếu chọn sai.

onTilePress = (rowIndex, columnIndex) => {
  console.log(`row ${rowIndex} column ${columnIndex} pressed!`)
}

Và thêm sự kiện:

onPress={() => this.onTilePress(rowIndex, columnIndex)}
…
onTilePress = (rowIndex, columnIndex) => {
  const { diffTileIndex, points, timeLeft } = this.state;
  if(rowIndex == diffTileIndex[0] && columnIndex == diffTileIndex[1]) {
    // good tile
    this.setState({ points: points + 1, timeLeft: timeLeft + 2 });
    // Gọi hàm tăng kích thước grid: generateNewRound
  } else {
    // wrong tile
    this.setState({ timeLeft: timeLeft - 2 });
  }
}

Nếu bạn muốn tăng kích thước grid thì thêm đoạn code sau:

generateNewRound = () => {
   const RGB = generateRGB();
   const mRGB = mutateRGB(RGB);
   const { points } = this.state;
   const size = Math.min(Math.max(Math.floor(Math.sqrt(points)), 2), 5);
   this.setState({
     size,
     diffTileIndex: [
       this.generateSizeIndex(size),
       this.generateSizeIndex(size),
     ],
     diffTileColor: `rgb(${mRGB.r}, ${mRGB.g}, ${mRGB.b})`,
     rgb: RGB,
   });
 };

Vậy là tạm hoàn thành rồi đấy, bạn thử build và chơi thử xem sao.

Bạn có thể tải mã nguồn tại đây:

Hẹn gặp lại ở bài viết theo nhé!

Xem tiếp các bài trong Series
Phần trước: Phần 2. Xây dựng màn hình Home của Game 2D
Dịch vụ phát triển ứng dụng mobile giá rẻ - chất lượng

Bình luận. Cùng nhau thảo luận nhé!

avatar
  Theo dõi bình luận  
Thông báo