Python开发接水果小游戏

Posted Yorhom

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python开发接水果小游戏相关的知识,希望对你有一定的参考价值。

我研发的Python游戏引擎Pylash已经更新到1.4了。现在我们就来使用它完成一个极其简单的小游戏:接水果。以下是游戏截图:

接水果 游戏截图

游戏操作说明:点击屏幕左右两边或者使用键盘方向键控制人物移动,使人物与水果接触得分,碰到非水果的物品,如碎玻璃,就会game over。

接下来是详尽的开发过程,篇幅较长,请看官耐心阅读。

Pylash项目地址

由于本次开发用到了pylash,大家可以先去Github上对引擎进行了解。
https://github.com/yuehaowang/pylash_engine

创建项目

首先在工作目录创建一个名为get_fruits的目录。然后到Github下载Pylash。引擎是基于Python3和PyQt4构建的,所以在使用前请确保你使用的是Python3并且安装了PyQt4。如果没有,可以在上述项目地址中找到他们的相关网页链接进行下载安装,安装和配置步骤都十分简单。这里不再赘述。

下载完Pylash后,我们得到这样的目录结构:

+- pylash_engine/
    |
    +- pylash/
    |
    +- demo/
    |
    +- examples/

大家可以在demo/examples/两个目录下查看示例。本文的源代码可以在examples/get_fruits中找到。

pylash目录就是引擎源代码。接下来把这个目录复制到我们创建的get_fruits目录下,再在get_fruits目录下创建一个images目录,用于储存图片。最后创建一个Main.py文件。这时,我们的get_fruits目录结构如下:

+- get_fruits/
    |
    +- pylash/
    |
    +- images/
    |
    +- Main.py

然后将引擎目录plash_engine/examples/get_fruits/images/下图片复制到项目目录get_fruits/images/下,用作游戏素材。

images/目录 截图

这样一来,我们的项目就创建好了,接下来只用往Main.py里填写代码,然后运行即可。

编写Hello World小程序

用代码编辑器(推荐Sublime Text)打开Main.py文件,写入以下代码:

# !/usr/bin/env python3
# -*- coding: utf-8 -*-

from pylash.utils import init, addChild
from pylash.text import TextField

def main():
    # 创建文本显示对象
    txt = TextField()
    # 设置文本内容
    txt.text = "Hello World"
    # 设置文本颜色
    txt.textColor = "red"
    # 设置文本位置
    txt.x = 50
    txt.y = 100
    # 设置文本字体大小
    txt.size = 50
    # 将文本对象加入到最底层
    addChild(txt)

# 初始化窗口,参数:界面刷新时间(单位:毫秒),窗口标题,窗口宽度,窗口高度,初始化完毕后回调函数
init(1000 / 60, "Hello World", 800, 600, main)

运行Main.py,如果得到了如下图所示的界面,说明程序正常运转起来了。

Hello World 演示截图

大家可以结合注释初步认识Pylash。熟悉flash的同学不难发现,TextField就是flash里显示文本的类,而且用法十分相近。
我们从代码的第4行看起,这里我们引入了pylash中的一些函数和类。pylash提供了很多模块,大家可以到这里查看它们的简介。
再往下看,我们会发现,pylash提供了一个用于显示文本的类,通过设置这个类的不同属性来设定文本样式。最后使用addChild将文本显示对象加入到界面中。我们可以把游戏看作分为很多层:地图层、人物层、UI层……,通过分层我们就能实现层次化显示效果。比如人物一直是在地图上方显示的,那么人物层就在地图层上方。addChild函数就是把一个显示对象加到最底层。
最后,我们使用init函数初始化窗口。

Pylash提供了许多基础显示对象,除了TextField文本显示类,还有Bitmap图片显示类,Sprite精灵类等。下文会提及。

编写游戏

有了上述对pylash的大致了解,我们就可以开始编写游戏了。首先,删除第四行以后所有代码。

引入所需

首先引入我们所需的所有类和函数,修改Main.py

from pylash.utils import stage, init, addChild, KeyCode
from pylash.system import LoadManage
from pylash.display import Sprite, BitmapData, Bitmap, FPS
from pylash.text import TextField, TextFormatWeight
from pylash.events import MouseEvent, Event, KeyboardEvent
from pylash.ui import LoadingSample1

这些类和函数在下面的代码中都会被用到。由于我是提前写好了游戏,所以在这里把这部分代码一块儿贴出来了,大家使用的时候可以根据自己使用情况,每用一个引入一个。

全局变量

游戏中需要用到一些全局变量,大家可以先浏览一遍,不同知道它们是干什么的,后文会用到它们:

dataList = {}

stageLayer = None
player = None
itemLayer = None
scoreTxt = None
addItemSpeed = 40
addItemSpeedIndex = 0
score = 0
keyboardEnabled = False

加载资源

我们的游戏中要用到图片,所以要提前加载图片(存储于images/目录下)。加载图片我们用到LoadManage静态类和LoadingSample1进度条类(还有LoadingSample2LoadingSample3这两款不同样式的进度条。或者大家深入学习后,可以自己写一个进度条类)。修改main函数:

def main():
    # 资源列表,一个list对象,格式:{"name" : 资源名称, "path" : 资源路径}
    loadList = [
        {"name" : "player", "path" : "./images/player.png"},
        {"name" : "bg", "path" : "./images/bg.jpg"},
        {"name" : "item0", "path" : "./images/item0.png"},
        {"name" : "item1", "path" : "./images/item1.png"},
        {"name" : "item2", "path" : "./images/item2.png"},
        {"name" : "item3", "path" : "./images/item3.png"},
        {"name" : "item4", "path" : "./images/item4.png"},
        {"name" : "item5", "path" : "./images/item5.png"},
        {"name" : "item6", "path" : "./images/item6.png"},
        {"name" : "item7", "path" : "./images/item7.png"}
    ]

    # 创建进度条
    loadingPage = LoadingSample1()
    addChild(loadingPage)

    # 加载完成后调用的函数,接受一个参数,该参数是一个dict对象,通过result[资源名称]来获取加载完成的资源
    def loadComplete(result):
        # 调用remove方法从界面上移除自身
        loadingPage.remove()

        # 调用初始化游戏函数
        gameInit(result)

    # 加载文件,参数:资源列表,每加载完一个资源回调函数(多用于显示进度),加载完所有资源回调函数
    LoadManage.load(loadList, loadingPage.setProgress, loadComplete)

上述代码含有详细注释,理解起来应该不算困难。可以看到,我们使用LoadManage.load实现加载。LoadingSample1.setProgress用于设置显示进度。

创建开始界面

我们在main函数中调用了gameInit函数,所以添加该函数:

def gameInit(result):
    global dataList, stageLayer

    # 保存加载完成的资源,这样一来,就可以使用dataList[资源名称]来获取加载完成的资源
    dataList = result

    # 创建舞台层
    stageLayer = Sprite()
    addChild(stageLayer)

    # 加入FPS,方便查看游戏效率
    fps = FPS()
    addChild(fps)

    # 加入背景图片
    bg = Bitmap(BitmapData(dataList["bg"]))
    stageLayer.addChild(bg)

    # 加入文本
    titleTxt = TextField()
    titleTxt.text = "Get Furit"
    titleTxt.size = 70
    titleTxt.textColor = "red"
    titleTxt.x = (stage.width - titleTxt.width) / 2
    titleTxt.y = 100
    stageLayer.addChild(titleTxt)

    hintTxt = TextField()
    hintTxt.text = "Tap to Start the Game!~"
    hintTxt.textColor = "red"
    hintTxt.size = 40
    hintTxt.x = (stage.width - hintTxt.width) / 2
    hintTxt.y = 300
    stageLayer.addChild(hintTxt)

    engineTxt = TextField()
    engineTxt.text = "- Powered by Pylash -"
    engineTxt.textColor = "red"
    engineTxt.size = 20
    engineTxt.weight = TextFormatWeight.BOLD
    engineTxt.italic = True
    engineTxt.x = (stage.width - engineTxt.width) / 2
    engineTxt.y = 500
    stageLayer.addChild(engineTxt)

    # 加入鼠标点击事件:点击舞台层后,开始游戏
    stageLayer.addEventListener(MouseEvent.MOUSE_UP, startGame)

    # 加入键盘事件:用于控制游戏中的人物
    stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDown)
    stage.addEventListener(KeyboardEvent.KEY_UP, keyUp)

def startGame(e):
    print("start game")

def keyDown(e):
    print("key down")

def keyUp(e):
    print("key up")

上述代码中,我们需要突破以下几个难点:

  1. Sprite精灵类。Sprite是一个精灵类。可是什么是精灵?其实你可以把它理解为一个层。它拥有addChild方法,用于把显示对象加到自身这个层上(和全局的addChild函数类似)。当然Sprite不只是有层的功能,不过你姑且把它看作一个层吧。

  2. BitmapBitmapData类的使用。Bitmap在上文中提到是一个用于显示图片的类。和TextField一样,使用addChild将它加入界面。BitmapData类是用于储存图像数据的,它接收的参数就是加载完成的图片资源。将他作为参数传给Bitmap类的构造器,就能创建出图片。BitmapData还可以进行像素操作,不过这是较高级的功能,目前不用了解。

  3. 事件。在pylash中,使用addEventListener统一接口加入事件,该方法参数:事件类型,事件监听器(即事件回调函数)。什么是事件呢?类似于一个信号,这个信号在某种情况下被发送后,指定的信号监听器就会被触发。这里添加鼠标事件的addEventListener是在EventDispatcher中定义的,DisplayObject类继承自EventDispatcher,所以继承自DisplayObject的所有类,都能加入事件。不过只有Sprite才有触发鼠标事件的能力。所以我们给stageLayer(舞台层,一个Sprite对象)加入了鼠标点击事件(MouseEvent.MOUSE_UP)。对应addEventListener方法的有removeEventListener(移除事件,参数相同)。鼠标事件除了MouseEvent.MOUSE_UP(鼠标弹起),还有MouseEvent.MOUSE_DOWN(鼠标按下),MouseEvent.MOUSE_MOVE(鼠标移动),MouseEvent.MOUSE_OUT(鼠标移出)等事件。后文会用到一些。事件的监听器是一个函数,startGamekeyDownkeyUp它们都是事件监听器。监听器在事件触发时被调用,并接受一个事件数据参数(通常写为e),通过这个参数可以获取事件的一些信息,如鼠标事件的监听器可以通过该参数获取鼠标位置。

  4. stage全局类。这里的stage是一个全局类,用于管理整个窗口,比如设置窗口刷新速度、获取窗口尺寸(stage.width,stage.height),有点类似于javascript里的window。键盘事件总不能加到某个对象上吧,所以stage还能加入键盘事件。加入键盘事件同样使用addEventListener这个的统一接口。

最后加入init函数初始化窗口:

init(1000 / 60, "Get Fruits", 800, 600, main)

init函数中,值得注意的是第一个参数,上文代码的注释中解释的是“界面刷新时间”,也就是说我们的界面是在不断刷新重绘的。这个参数就是用来决定刷新的时间。参数值越小,刷新得越快,游戏越流畅,不过也不用设置得太小,太小了话,刷新速度过快,设备会跟不上这个节奏的。玩过游戏的朋友可以这么理解这个参数,用1000除以这个参数,得到的就是FPS。

运行Main.py,得到如下界面:

接水果开始界面 截图

可以看到,我们的界面上有图片也有文本。点击界面输出“start game”,按下键盘输出“key down”,释放键盘输出“key up”。这样一来,我们就成功地添加了显示对象和鼠标&键盘事件。

开始游戏

舞台层鼠标点击事件的监听器是startGame函数,也就是说,我们点击开始界面就开始游戏。修改startGame函数:

def startGame(e):
    global player, itemLayer, scoreTxt, addItemSpeedIndex, score, keyboardEnabled

    # 初始一些全局变量
    addItemSpeedIndex = 0
    score = 0

    keyboardEnabled = True

    # 清空舞台层和舞台事件
    stageLayer.removeAllChildren()
    stageLayer.removeAllEventListeners()

    # 加入背景
    bg = Bitmap(BitmapData(dataList["bg"]))
    stageLayer.addChild(bg)

    # 创建角色
    player = Player(dataList["player"])
    player.x = (stage.width - player.width) / 2
    player.y = 450
    stageLayer.addChild(player)

    # 创建下落物品层
    itemLayer = Sprite()
    stageLayer.addChild(itemLayer)
    # 将人物对象保存到itemLayer中,用于检测碰撞
    itemLayer.hitTarget = player

    # 加入分数文本
    scoreTxt = TextField()
    scoreTxt.text = "Score: 0"
    scoreTxt.textColor = "red"
    scoreTxt.size = 30
    scoreTxt.x = scoreTxt.y = 30
    scoreTxt.weight = TextFormatWeight.BOLDER
    stageLayer.addChild(scoreTxt)

    # 加入事件
    stageLayer.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDown)
    stageLayer.addEventListener(MouseEvent.MOUSE_UP, onMouseUp)
    stageLayer.addEventListener(Event.ENTER_FRAME, loop)

def onMouseDown(e):
    print("mouse down")

def onMouseUp(e):
    print("mouse up")

def loop(e):
    print("loop")

对应addChildSprite提供了removeChild方法用于移除显示对象。除此之外还有removeAllChildren移除所有对象方法。removeAllEventListeners顾名思义就是移除所有事件。上面的代码会让人一头雾水,同样的,我们需要突破以下难关:

  1. 全局变量。addItemSpeedIndex是用于控制添加下落物品的时间间隔,后文会提及。score是保存分数的变量。由于游戏开始后,这些变量要回到初始值,所以在startGame函数中添加了这些代码来完成这项任务。keyboardEnabled = True这行代码是用于打开键盘事件,键盘事件是加到stage对象上的(见上文),但是是用于操作游戏中主角的,所以只有在游戏开始后才有用,所以加入keyboardEnabled变量作为能否使用键盘的开关,后文修改键盘事件监听器时会用到它。

  2. Player类。这个类是我们要自己创建的人物类,后文会展示其代码。

  3. 时间轴事件ENTER_FRAME。我们了解了鼠标事件,认识MouseEvent.MOUSE_DOWNMouseEvent.MOUSE_UP,可是Event.ENTER_FRAME是什么东西-_-#?这个事件就是时间轴事件。时间轴事件类似于一个计时器。这个事件的监听器每隔段事件就会触发。事件触发的时间间隔取决于init函数的第一个参数。

运行代码,点击开始界面开始游戏,你可以发现控制台在不停地输出“loop”,代表时间轴事件运转了。

Player人物类

上文提到了这个类,在写这个类之前,我们重新在get_fruits/目录下创建一个名为Player.py的python文件。创建完成后,打开这个文件,加入以下代码:

from pylash.utils import stage
from pylash.display import Sprite, Animation, BitmapData

# 创建Player类,并使其继承自Sprite类
class Player(Sprite):
    def __init__(self, playerImage):
        super(Player, self).__init__()

        # 移动方向,【right向右,left向左,None不移动】
        self.direction = None
        # 移动速度
        self.step = 5

        # 创建图片数据
        bmpd = BitmapData(playerImage)
        # 创建动画帧列表
        frames = Animation.divideUniformSizeFrames(bmpd.width, bmpd.height, 4, 4)

        # 创建动画
        self.animation = Animation(bmpd, frames)
        # 设置动画播放速度
        self.animation.speed = 5
        # 播放动画
        self.animation.play()
        # 将动画加入界面
        self.addChild(self.animation)

    def loop(self):
        # 向右移动
        if self.direction == "right":
            self.x += self.step
            # 播放向右移动时的动画
            self.animation.currentRow = 2
        # 向左移动
        elif self.direction == "left":
            self.x -= self.step
            # 播放向左移动时的动画
            self.animation.currentRow = 1
        # 不移动
        else:
            # 播放不移动时的动画
            self.animation.currentRow = 0

        # 限制人物位置
        if self.x < 0:
            self.x = 0
        elif self.x > stage.width - self.width:
            self.x = stage.width - self.width

这个Player类需要继承自Sprite,使其成为一个显示对象。也就是说继承自Sprite后,就可以被addChild到界面上去了,并可以显示出来。除此之外,还可以使用Player对象的addChild方法来向人物类添加显示元件。Player类的构造器接收一个人物图片参数。
代码中用到了Animation类。它由pylash提供,用于创建简单的基于图片的动画。Animation构造器接收两个参数:动画位图数据,动画帧列表。

一般而言,我们的动画用的图片都是这样的:

动画图片

所以我们播放动画的时候,只需要控制位图显示区域的大小和位置就能实现播放动画。类似于放映机放映电影。如下两幅图所示,显示区域就是空白部分,不被显示的区域就是被半透明黑色幕布遮住的部分。动画中的每个小图叫帧,移动显示区域就实现切换帧,达到播放动画的目的。

动画播放示例之一

动画播放示例之二

代码中的Animation.divideUniformSizeFrames(bmpd.width, bmpd.height, 4, 4)就是用于获取每帧的位置和大小。divideUniformSizeFrames静态方法接收四个参数:动画图片宽度,动画图片高度,动画列数,动画行数。该方法只适合得到每帧分布和大小都是均匀的帧列表。

Animation有个speed属性,用于控制动画播放速度。如果不设置这个属性,动画中每帧的切换速度就和init中设置的刷新速度一样。设置后,切换速度变为speed * 刷新速度。

Animation类默认只播放第一排第一行动画,要指定动画播放的位置,需要设置currentRowcurrentColumn属性来控制播放的行和列。

下落物品类:Item

这个类在前面没出现过,不过我们先写好放在这里,下文要用到。同样的,新建一个名为Item.py的文件,打开它,写入代码:

from pylash.utils import stage
from pylash.display import Sprite, Bitmap, BitmapData

class Item(Sprite):
    # 定义自定义事件
    EVENT_ADD_SCORE = "event_add_score"
    EVENT_GAME_OVER = "event_game_over"

    def __init__(self, image):
        super(Item, self).__init__()

        bmp = Bitmap(BitmapData(image))
        self.addChild(bmp)

        self.index = 0
        self.y = -bmp.height / 2

    def loop(self):
        player = None

        # 获取人物对象
        if self.parent:
            player = self.parent.hitTarget

        if player is None:
            return

        # 向下移动
        self.y += 5

        # 碰撞检测
        if (abs(self.x + self.width / 2 - player.x - player.width / 2) <= (self.width + player.width) / 2) and (abs(self.y + self.height / 2 - player.y - player.height / 2) <= (self.height + player.height) / 2):
            # 如果index <= 3,代表物品是水果
            if self.index <= 3:
                # 触发自定义事件:加分事件
                self.dispatchEvent(Item.EVENT_ADD_SCORE)

                self.remove()
            # 如果物品是非水果
            else:
                # 触发自定义事件:游戏结束
                self.dispatchEvent(Item.EVENT_GAME_OVER)

        # 移除自身,当自身移出了屏幕
        if self.y >= stage.height:
            self.remove()

Item类的构造器和Player构造器一样,接受一个图片参数。
我们这里用到了一个比较高级的功能:自定义事件。自定的事件可以是一个字符串,作为该事件的标识。使用dispatchEvent方法触发事件。dispatchEvent方法在EventDispatcher中定义,通过继承使Item也能使用这个方法。
值得关注的还有检测碰撞部分。目前处理简单的矩形碰撞即可。首先来看张图:

矩形碰撞检测演示图

如果要横向判断碰撞的话,判断(x1-x2)的绝对值是否小于或者等于w1/2+w2/2,如果是则横向则有碰撞。纵向判断是一样的,判断(y1-y2)的绝对值是否小于或等于h1/2+h2/2即可。

修改事件监听器

上面的代码中我们虽然添加了事件,但是没有添加有效的事件监听器,所以修改这些函数:

def keyDown(e):
    global player

    if not keyboardEnabled or not player:
        return

    if e.keyCode == KeyCode.KEY_RIGHT:
        player.direction = "right"
    elif e.keyCode == KeyCode.KEY_LEFT:
        player.direction = "left"

def keyUp(e):
    global player

    if not keyboardEnabled or not player:
        return

    player.direction = None

def onMouseDown(e):
    global player

    if e.offsetX > (stage.width / 2):
        player.direction = "right"
    else:
        player.direction = "left"

def onMouseUp(e):
    global player

    player.direction = None

def loop(e):
    global player, itemLayer, addItemSpeed, addItemSpeedIndex

    player.loop()

    for o in itemLayer.childList:
        o.loop()

    # 控制添加下落物品时间间隔
    if addItemSpeedIndex < addItemSpeed:
        addItemSpeedIndex += 1

        return

    addItemSpeedIndex = 0

    # 获得随机下落物品
    randomNum = random.randint(0, 7)

    # 加入下落物品
    item = Item(dataList["item" + str(randomNum)])
    item.index = randomNum
    item.x = int(random.randint(30, stage.width - 100))
    itemLayer.addChild(item)
    # 加入自定义的事件
    item.addEventListener(Item.EVENT_ADD_SCORE, addScore)
    item.addEventListener(Item.EVENT_GAME_OVER, gameOver)

keyDownkeyUponMouseDownonMouseUp这四个监听器用于操作人物(player)。

接下来看监听器loop。该函数中,首先调用了人物的loop方法(见Player类的loop)。我们在上文定义的itemLayer是一个Sprite对象,Sprite对象有一个childList属性,是一个list对象,保存了所有的子对象。所以我们通过遍历itemLayer的这个列表,获取每个下落物品,调用它们的loop方法。接下来使用addItemSpeedIndexaddItemSpeed两个全局变量控制加入下落物品的速度。接下来的代码就是来构造Item类创建下落物品。

加分和Game Over

我们给Item对象加入了自定义事件,分别触发addScoregameOver监听器,加入这两个监听器:

def addScore(e):
    global score, scoreTxt

    score += 1

    scoreTxt.text = "Score: %s" % score

def gameOver(e):
    global player, scoreTxt, stageLayer, keyboardEnabled

    keyboardEnabled = False

    stageLayer.removeAllEventListeners()

    scoreTxt.remove()
    player.animation.stop()

    resultTxt = TextField()
    resultTxt.text = "Final Score: %s" % score
    resultTxt.size = 40
    resultTxt.weight = TextFormatWeight.BOLD
    resultTxt.textColor = "orangered"
    resultTxt.x = (stage.width - resultTxt.width) / 2
    resultTxt.y = 250
    stageLayer.addChild(resultTxt)

    hintTxt = TextField()
    hintTxt.text = "Double Click to Restart"
    hintTxt.size = 35
    hintTxt.textColor = "red"
    hintTxt.x = (stage.width - hintTxt.width) / 2
    hintTxt.y = 320
    stageLayer.addChild(hintTxt)

    # 加入双击事件,点击后重新开始游戏
    stageLayer.addEventListener(MouseEvent.DOUBLE_CLICK, startGame)

运行Main.py,开始游戏后,得到本文开篇图片所示效果。移动人物,接触下落的物品。如果碰到碎玻璃等非水果物品就会game over:

接水果游戏Game Over 截图

Ok,我们的接水果小游戏就完成了。可见使用python+pylash开发小游戏还是很方便的。

源代码

本文的源代码可以在引擎目录的examples/get_fruits中找到。或者到这里在线查看。

文中有任何不妥之处或者读者有疑问的话,欢迎大家交流~


欢迎大家继续关注我的博客

转载请注明出处:Yorhom’s Game Box

http://blog.csdn.net/yorhomwang

以上是关于Python开发接水果小游戏的主要内容,如果未能解决你的问题,请参考以下文章

Python开发接水果小游戏

Python开发接水果小游戏

使用Python开发一个超级简单的接水果小游戏,零基础也可以学会

Python游戏开发,pygame模块,Python实现经典吃豆豆小游戏

BZOJ4009 HNOI2015 接水果

Python游戏开发,pygame模块,Python实现俄罗斯方块小游戏