效果图
最近一直在搞小程序,之前学过的kotlin都快忘光了。借此采用kotlin写了个贪吃蛇的demo,来回顾下kotlin的语法。
分析
贪吃蛇游戏由食物(红色小块)和小蛇(绿色小块)组成。其中需要明确的问题如下:
小蛇吃到食物后,食物在新的坐标重新出现(坐标需在显示区内)
小蛇吃到食物后身体变长(长度为食物的长度)
蛇身应由一个个小正方形组成大小与食物一样
蛇头和蛇尾不能调转即蛇头方向向左则无法再向右移动
小蛇在拐弯时,蛇身跟随蛇头转向即蛇身到达蛇头所在位置时如果与蛇头方向不一直则改变蛇身方向
小蛇碰撞到墙壁(蛇头不在显示区域内)或蛇头碰撞到蛇身则游戏结束
代码
class Food() : View{
companion object {
val widthAndHeight = 20
}
var isShow: Boolean = true
var color: Color = Color.RED
var x: Int = 0;
var y: Int = 0;
override fun draw(g: Graphics?) {
g!!.color = if(isShow) color else null
isShow = !isShow
g.fillRect(x,y,widthAndHeight,widthAndHeight)
}
/**
* 生成食物
*/
fun generateFood(cellList: ArrayList<Snake.Cell>){
while(true){
val point = GameUtils.generatePosition()
var isSuccess = true;
for(cell in cellList){
if(cell.x == point.x && cell.y == point.y){ //排除小蛇的坐标
isSuccess = false
}
}
if(isSuccess){
x = point.x
y = point.y
break
}
}
}
}
class Snake() : View{
enum class Direction{ //方向枚举
TOP,RIGHT,BOTTOM,LEFT
}
var cellList: ArrayList<Cell> = ArrayList() //组成蛇的小正方形列表
var color: Color = Color.GREEN //蛇的颜色
private var inflectionPoint: ArrayList<Cell> = ArrayList() //记录发生转向的蛇头
init {
initCellList()
}
fun initCellList(){
cellList.clear()
cellList.add(Cell(0,0, Direction.LEFT)) //添加蛇头
}
/**
* 检查是否成功进食,是=增加蛇身长度,否则不做处理
*/
fun tryEat(food: Food): Boolean{
if(cellList[0].x == food.x && cellList[0].y == food.y){
val lastCell = cellList[cellList.size - 1];
when(lastCell.direction){
Direction.LEFT-> cellList.add(Cell(lastCell.x + Food.widthAndHeight,lastCell.y,lastCell.direction))
Direction.RIGHT-> cellList.add(Cell(lastCell.x - Food.widthAndHeight,lastCell.y,lastCell.direction))
Direction.TOP-> cellList.add(Cell(lastCell.x,lastCell.y + Food.widthAndHeight,lastCell.direction))
Direction.BOTTOM-> cellList.add(Cell(lastCell.x,lastCell.y - Food.widthAndHeight,lastCell.direction))
}
return true
}
return false
}
fun walk(){
for(index in cellList.indices){
val cell:Cell = cellList[index]
if(index != 0){
for(pCell in inflectionPoint){//判断蛇身的每一个正方形是否经过蛇头转向点,是改变方向
if(cell.x == pCell.x && cell.y == pCell.y){
cell.direction = pCell.direction
println("change direction")
if(index == cellList.size-1){//当蛇尾也经过蛇头转向点时,从列表删除转向信息
inflectionPoint.remove(pCell)
}
break;
}
}
}
when(cell.direction){//根据方向修改坐标,进行移动操作
Snake.Direction.TOP-> cell.y -= Food.widthAndHeight
Snake.Direction.RIGHT-> cell.x += Food.widthAndHeight
Snake.Direction.BOTTOM-> cell.y += Food.widthAndHeight
Snake.Direction.LEFT-> cell.x -= Food.widthAndHeight
}
}
}
/**
* 根据输入的#{switchDirection}进行转向操作
* @param switchDirection 方向枚举
*/
fun switchDirection(switchDirection:Snake.Direction){
val cDirection = cellList[0].direction
when(switchDirection){
Snake.Direction.LEFT->{
if(cDirection != Snake.Direction.RIGHT){
cellList[0].direction = Snake.Direction.LEFT
}
}
Snake.Direction.TOP->{
if(cDirection != Snake.Direction.BOTTOM){
cellList[0].direction = Snake.Direction.TOP
}
}
Snake.Direction.RIGHT->{
if(cDirection != Snake.Direction.LEFT){
cellList[0].direction = Snake.Direction.RIGHT
}
}
Snake.Direction.BOTTOM->{
if(cDirection != Snake.Direction.TOP){
cellList[0].direction = Snake.Direction.BOTTOM
}
}
}
if(cellList.size > 1){
inflectionPoint.add(Cell(cellList[0].x,cellList[0].y,cellList[0].direction))
}
}
/**
* 获取蛇头
*/
fun getHeaderCell(): Cell{
return cellList[0]
}
fun getCellCount(): Int{
return cellList.size
}
/**
* 是否咬到自己(蛇头碰撞到蛇身)
*/
fun biteYourself(): Boolean{
for(index in cellList.indices){
if(index != 0){
val cell = cellList[index]
if(getHeaderCell().x == cell.x && getHeaderCell().y == cell.y){
return true
}
}
}
return false
}
fun generatePosition(){
val point = GameUtils.generatePosition()
cellList[0].x = point.x
cellList[0].y = point.y
if(point.x <= Food.widthAndHeight * 2){
cellList[0].direction = Direction.RIGHT
}
}
override fun draw(g: Graphics?) {
g!!.color = color
for(cell in cellList){
g.fillRect(cell.x,cell.y,cell.width,cell.height)
}
}
/**
* 蛇的每一节,每吃一个食物涨一节
*/
data class Cell(var x:Int,var y:Int,var direction: Direction,var width: Int = Food.widthAndHeight,var height: Int = Food.widthAndHeight)
}
/**
* 游戏主界面
*/
class MainFrame(width:Int,height:Int) : DoubleBufferFrame(width,height), KeyListener {
var isConfirmStart = false; //是否确认开始
var food: Food = Food()
var snake: Snake = Snake()
init {
initData()
title = "贪吃蛇"
addKeyListener(this)
if(JOptionPane.showOptionDialog(this,"点击确定开始游戏","提示",JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE,null,null,null) == 0){
isConfirmStart = true
}
}
fun initData(){
food.generateFood(snake.cellList)
snake.generatePosition()
}
override fun updateAttr() {
if(!isConfirmStart){
return
}
if(isOver()){
isStopDrawThread = true;
if(JOptionPane.showOptionDialog(this,"游戏结束,得分${(snake.getCellCount()-1) * SCORE},是否重新开始","提示",JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE,null,null,null) == 0){
snake.initCellList()
initData()
isStopDrawThread = false;
}
return
}
if(snake.tryEat(food)){
food.generateFood(snake.cellList)
}
snake.walk()
}
/**
* 蛇撞墙或着咬到自己则返回true,游戏结束
*/
fun isOver(): Boolean{
return isCrashWall() || snake.biteYourself()
}
/**
* 蛇是否碰撞到墙
*/
fun isCrashWall(): Boolean{
val cell:Snake.Cell = snake.getHeaderCell();
return cell.x < contentPane.x || cell.x + cell.width > contentPane.width || cell.y < contentPane.y || cell.y + cell.height > contentPane.height
}
override fun dPaint(g: Graphics?) {
food.draw(g)
snake.draw(g)
}
override fun keyTyped(e: KeyEvent?) {
}
override fun keyPressed(e: KeyEvent?) {
isStopDrawThread = true
when(e!!.keyCode){
37-> snake.switchDirection(Snake.Direction.LEFT)
38-> snake.switchDirection(Snake.Direction.TOP)
39-> snake.switchDirection(Snake.Direction.RIGHT)
40-> snake.switchDirection(Snake.Direction.BOTTOM)
}
repaint()
}
override fun keyReleased(e: KeyEvent?) {
isStopDrawThread = false
}
}
DoubleBufferFrame类采用双缓冲技术解决闪烁问题,主要思路是先将需绘制的场景绘制到内存,再从内存绘制到窗体上
const val BLOCK_LENGTH:Int = 20
const val SCORE:Int = 10 //每吃一个食物的得分
const val DIFFICULTY:Int = 5 //难度 1-10
fun main(args: Array<String>){
MainFrame(Food.widthAndHeight * BLOCK_LENGTH,Food.widthAndHeight * BLOCK_LENGTH)
}
总结
贪吃蛇的实现并不难,几个类就能实现了。估计也就只有分析中的第5点需要稍微思考下。有兴趣的朋友可以拓展下。附上源码地址: https://github.com/aii1991/GluttonousSnake