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:
- [Game 2D-React Native] Xây dựng màn hình Home của game – Phần 2
- [Series] Phát triển Game 2D hoàn chỉnh bằng React Native
Nội dung chính của bài viết
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ỏ.
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>
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.
Để 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:
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.
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:
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é!
Bình luận. Cùng nhau thảo luận nhé!