写在比前面还前面的话:
TD 杂谈08 | 中的LidarTracker工具测试试用时间已经到啦,十分感谢在测试期间使用LidarTracker的各位朋友,感谢各位在测试期间的反馈。如果日后有需要使用LidarTracker工具的话,请与我们联系。如果你在使用Touchdesigner中有任何需求需要做成工具,也欢迎与我们联系。另外,新版本的LidarTracker我们也在计划中,如果你对在Touchdesigner中使用雷达有一些希望做到的功能的话,可以向我们建议噢,我们会考虑加入开发内容。
- 写在前面的废话 -
- 为什么做了个贪吃蛇
主要是因为作者最近沉迷switch不能自拔,摸到键盘就想起游戏,正好带着个小朋友,就做个贪吃蛇教小朋友,顺便让小朋友也体会一下打游戏的乐趣。
这次的贪吃蛇主要是用代码实现的主要逻辑,所以对于TD脚本写不好的朋友,你们可以稍微紧张的往下看。
假装这是一条注释
- 制作思路 -- 治大国写程序如烹小鲜
就像做菜一样,首先得要有菜谱(程序逻辑),然后需要有食材与锅碗瓢盆(数据与运行环境)。准备好这些要素,能才能开火炒菜(写脚本)。
- 程序逻辑贪吃蛇其实并不复杂,游戏程序共3种状态,待机,游戏中,游戏结束,游戏程序始终在这3种中的一个状态运行。
在待机状态,我这里设定需要显示一个待机画面并等待键盘里的方向键按下。在方向键按下之后,待机状态解除,进入游戏状态,小蛇就开始它变成巨蟒的道路,在游戏中的状态时候,如果发生小蛇碰撞到自己身体或者小蛇已经成长到屏幕都容不下的时候,游戏结束,进入游戏结束状态,游戏结束的时候像素点从下往上依次点亮,满屏之后回到待机状态。
成品
- 数据与运行环境这个程序主要画面的渲染我是使用GeometryCOMP的Instance方法实现的,对Instance方法不熟悉的朋友可以移步TEA社区的中文视频教程,里面有介绍。
http://www.touchdesigner.co/intermediate
TEA社区中级TD教程网址
通过Grid SOP生成点阵数据
一个简单的思路就是,使用GridSOP生成一个矩形点阵数据,使用GeometryCOMP的Instance方法将点阵渲染出来,至于贪吃蛇的操作逻辑,我就可以通过操作点阵中的每个点的scale与RGBA值来实现。通过GeometryCOMP的Instance方法将点阵渲染出来
有了渲染画面的方法之后,接下来就是如何通过ScriptCHOP来生成数据的问题了。
为了使用ScriptCHOP生成数据,我们必须将一些需要定义的数据提前准备好,比如用于控制程序状态的gameState,用于控制小蛇前进方向的snakeDiraction等等。这些数据我都使用了ConstantCHOP来生成。
用于定义数据的ConstantCHOP
除此之外,还需要一个储存在游戏中小蛇不断变长的身体数据的地方。这里我使用了一个TableDAT,TableDAT脚本中的appendRow()方法和clear()方法可以比较方便的操作数据。储存小蛇身体数据的节点,使用DattoCHOP转化为CHOP节点是为了方便ScriptCHOP获取数据
有了以上这些基础的数据之后,我们还需要一个触发器,用于更新小蛇前进的速度,这里的触发器我是用LFOCHOP制作。游戏的速度等级可以通过调整LFO的Frequency属性调整
准备好以上节点之后,就可以将以上节点连入ScriptCHOP,开始编写ScriptCHOP脚本生成我们想要的数据了。TD代码写得效率高不高不重要,重要是界面一定要整齐(手动狗头)
写脚本
数据都准备完成了,接下来就是进入scriptCHOP写脚本了。完整的脚本如下:
def onSetupParameters(scriptOp): # 生成初始化按钮 page = scriptOp.appendCustomPage('Game Control') p = page.appendPulse('Init', label='Initialize Game') return def onPulse(par): if par.name == 'Init' : # 定义所需OP dataUpdateOp = op('dataUpdate') controlConsOp = op('controlCons') snakeDataTableOp = op('snakeDataTable') defineOp = op('define') # 重置触发器 dataUpdateOp.par.resetpulse.pulse() # 重置小蛇前进方向 controlConsOp.par.value1 = 0 # 重置小蛇身体数据 snakeDataTableOp.clear(keepFirstRow = True) initSnakeDataRow = defineOp['gameGridRow'][0] // 2 + 1 initSnakeDataCol = defineOp['gameGridCol'][0] // 2 - 2 snakeDataTableOp.appendRow([initSnakeDataRow, initSnakeDataCol - 1, defineOp['snakeColorR'][0], defineOp['snakeColorG'][0], defineOp['snakeColorB'][0], 0.4]) snakeDataTableOp.appendRow([initSnakeDataRow, initSnakeDataCol, defineOp['snakeColorR'][0], defineOp['snakeColorG'][0], defineOp['snakeColorB'][0], 1]) # 重置得分点数据 controlConsOp.par.value2 = op('define')['gameGridRow'][0] // 2 + 1 controlConsOp.par.value3 = op('define')['gameGridCol'][0] // 2 + 3 return def onCook(scriptOp): # 定义输入OP for OP in scriptOp.inputs: if OP.name == 'dataUpdate': dataUpdateOp = OP elif OP.name == 'gridData': gridDataOp = OP elif OP.name == 'control': controlOp = OP elif OP.name == 'define': defineOp = OP elif OP.name == 'snakeData': snakeDataOp = OP elif OP.name == 'endAnimation': endAnimationOp = OP try: dataUpdateOp gridDataOp controlOp defineOp snakeDataOp endAnimationOp except NameError: scriptOp.clear() return # 定义各种参数 updateSignal = dataUpdateOp['dataUpdate'][0] gameState = int(controlOp['gameState'][0]) snakeDiraction = int(controlOp['snakeDiraction'][0]) scorePointRow = int(controlOp['scorePointRow'][0]) scorePointCol = int(controlOp['scorePointCol'][0]) maxRow = int(defineOp['gameGridRow'][0]) maxCol = int(defineOp['gameGridCol'][0]) snakeColorR = int(defineOp['snakeColorR'][0]) snakeColorG = int(defineOp['snakeColorG'][0]) snakeColorB = int(defineOp['snakeColorB'][0]) scorePointColorR = int(defineOp['scorePointColorR'][0]) scorePointColorG = int(defineOp['scorePointColorG'][0]) scorePointColorB = int(defineOp['scorePointColorB'][0]) scorePointIndex = (scorePointRow - 1) * maxCol + scorePointCol - 1 # 待机状态 if gameState == 0 : # 计算小蛇身体坐标位置 snakeDataIndex = [] for i in range(snakeDataOp.numSamples): index = int((snakeDataOp['row'][i] - 1) * maxCol + snakeDataOp['col'][i] - 1) snakeDataIndex.append(index) # 输出数据 scriptOp.clear() scaleChan = scriptOp.appendChan('scale') rChan = scriptOp.appendChan('r') gChan = scriptOp.appendChan('g') bChan = scriptOp.appendChan('b') aChan = scriptOp.appendChan('a') scriptOp.numSamples = gridDataOp.numSamples scaleChan[scorePointIndex] = 1 rChan[scorePointIndex] = scorePointColorR gChan[scorePointIndex] = scorePointColorG bChan[scorePointIndex] = scorePointColorB aChan[scorePointIndex] = 1 for i, val in enumerate(snakeDataIndex): if i == len(snakeDataIndex) - 1: scaleChan[val] = 1 else: scaleChan[val] = 0.9 rChan[val] = snakeDataOp['r'][i] gChan[val] = snakeDataOp['g'][i] bChan[val] = snakeDataOp['b'][i] aChan[val] = snakeDataOp['a'][i] return # 游戏中状态 elif gameState == 1: if updateSignal > 0.5: # 定义参数 snakeData = [] numSnakeData = snakeDataOp.numSamples snakeDataTableOp = op('snakeDataTable') controlConsOp = op('controlCons') # 获取旧的小蛇身体数据 for i in range(numSnakeData): snakeData.append([snakeDataOp['row'][i], snakeDataOp['col'][i]]) # 根据前进方向计算小蛇新的头部位置 snakeHeadData = snakeData[numSnakeData - 1] if snakeDiraction == 0: newSnakeHeadDataCol = snakeHeadData[1] + 1 if newSnakeHeadDataCol > maxCol: newSnakeHeadDataCol = 1 newSnakeHeadData = [snakeHeadData[0], newSnakeHeadDataCol] elif snakeDiraction == 1: newSnakeHeadDataRow = snakeHeadData[0] + 1 if newSnakeHeadDataRow > maxRow: newSnakeHeadDataRow = 1 newSnakeHeadData = [newSnakeHeadDataRow, snakeHeadData[1]] elif snakeDiraction == 2: newSnakeHeadDataCol = snakeHeadData[1] - 1 if newSnakeHeadDataCol < 1: newSnakeHeadDataCol = maxCol newSnakeHeadData = [snakeHeadData[0], newSnakeHeadDataCol] elif snakeDiraction == 3: newSnakeHeadDataRow = snakeHeadData[0] - 1 if newSnakeHeadDataRow < 1: newSnakeHeadDataRow = maxRow newSnakeHeadData = [newSnakeHeadDataRow, snakeHeadData[1]] # 判断小蛇新的头部位置有没有吃到得分点 if newSnakeHeadData[0] == scorePointRow and newSnakeHeadData[1] == scorePointCol: # 将小蛇新的头部位置加入小蛇的数据集合中 snakeData.append(newSnakeHeadData) # 如果小蛇身体长度达到最大,结束游戏 if len(snakeData) == gridDataOp.numSamples: controlConsOp.par.value0 = 2 op('timer1').par.start.pulse() return # 重置得分点位置 while True: mRandomRow = random.randint(1, maxRow) mRandomCol = random.randint(1, maxCol) mCheck = True for i in range(len(snakeData)): if mRandomRow == snakeData[i][0] and mRandomCol == snakeData[i][1]: mCheck = False break if mCheck: controlConsOp.par.value2 = mRandomRow controlConsOp.par.value3 = mRandomCol break # 没有吃到得分点的情况 else: # 将小蛇尾部的位置数据从小蛇的数据集合中删除 snakeData.pop(0) # 判断小蛇新的头部数据是否碰撞到身体,如是则结束游戏 for s in snakeData: if s[0] == newSnakeHeadData[0] and s[1] == newSnakeHeadData[1]: controlConsOp.par.value0 = 2 op('timer1').par.start.pulse() return # 没有碰撞到身体,将小蛇新的头部位置加入小蛇的数据集合中 snakeData.append(newSnakeHeadData) # 更新小蛇身体数据 snakeDataTableOp.clear(keepFirstRow = True) snakeDataIndex = [] snakeAlphaData = [] for i, data in enumerate(snakeData): alpha = 0.4 + (0.6 / (len(snakeData) - 1)) * i snakeDataIndex.append(int((data[0] - 1) * maxCol + data[1] - 1)) snakeAlphaData.append(alpha) snakeDataTableOp.appendRow([data[0], data[1], snakeColorR, snakeColorG, snakeColorB, alpha]) # 输出数据 scriptOp.clear() scaleChan = scriptOp.appendChan('scale') rChan = scriptOp.appendChan('r') gChan = scriptOp.appendChan('g') bChan = scriptOp.appendChan('b') aChan = scriptOp.appendChan('a') scriptOp.numSamples = gridDataOp.numSamples scaleChan[scorePointIndex] = 1 rChan[scorePointIndex] = scorePointColorR gChan[scorePointIndex] = scorePointColorG bChan[scorePointIndex] = scorePointColorB aChan[scorePointIndex] = 1 for i, val in enumerate(snakeDataIndex): if i == len(snakeDataIndex) - 1: scaleChan[val] = 1 else: scaleChan[val] = 0.9 rChan[val] = snakeColorR gChan[val] = snakeColorG bChan[val] = snakeColorB aChan[val] = snakeAlphaData[i] return # 游戏结束状态 elif gameState == 2: # 计算小蛇身体坐标位置 snakeDataIndex = [] for i in range(snakeDataOp.numSamples): index = int((snakeDataOp['row'][i] - 1) * maxCol + snakeDataOp['col'][i] - 1) snakeDataIndex.append(index) # 输出数据 scriptOp.clear() scaleChan = scriptOp.appendChan('scale') rChan = scriptOp.appendChan('r') gChan = scriptOp.appendChan('g') bChan = scriptOp.appendChan('b') aChan = scriptOp.appendChan('a') scriptOp.numSamples = gridDataOp.numSamples scaleChan[scorePointIndex] = 1 rChan[scorePointIndex] = scorePointColorR gChan[scorePointIndex] = scorePointColorG bChan[scorePointIndex] = scorePointColorB aChan[scorePointIndex] = 1 for i, val in enumerate(snakeDataIndex): if i == len(snakeDataIndex) - 1: scaleChan[val] = 1 else: scaleChan[val] = 0.9 rChan[val] = snakeDataOp['r'][i] gChan[val] = snakeDataOp['g'][i] bChan[val] = snakeDataOp['b'][i] aChan[val] = snakeDataOp['a'][i] for i in range(int(endAnimationOp[0][0] * maxCol)): scaleChan[i] = 0.9 rChan[i] = snakeColorR gChan[i] = snakeColorG bChan[i] = snakeColorB aChan[i] = 1 return # 未知状态,清空窗口 else: scriptOp.clear() return
ScriptCHOP的脚本编写完之后,贪吃蛇游戏就已经完成一大半啦。还剩下最后一个重要内容,就是游戏输入控制的脚本了。
在这个贪吃蛇程序中我使用了键盘的方向键作为游戏按键。获取键盘的方向键是否按下的事件我们可以使用KeyboardinDAT。
keyboardinDAT
# 根据输入的头部与颈部位置判断当前蛇头所朝的方向def diraction(head, neck): maxRow = op('define')['gameGridRow'][0] maxCol = op('define')['gameGridCol'][0] if head[0] == neck[0]: if head[1] > neck[1]: if head[1] == maxCol and neck[1] == 1: return 2 else: return 0 else: if head[1] == 1 and neck[1] == maxCol: return 0 else: return 2 if head[1] == neck[1]: if head[0] > neck[0]: if head[1] == maxRow and neck[0] == 1: return 3 else: return 1 else: if head[1] == 1 and neck[1] == maxRow: return 1 else: return 3def onKey(dat, key, character, alt, lAlt, rAlt, ctrl, lCtrl, rCtrl, shift, lShift, rShift, state, time, cmd, lCmd, rCmd): gameState = int(op('control')['gameState'][0]) controlConsOp = op('controlCons') # 游戏中状态 if gameState == 1: # 按键被按下时 if state == 1: # 定义参数 snakeDataOp = op('snakeData') snakeDataOpSamples = snakeDataOp.numSamples snakeHeadData = [snakeDataOp['row'][snakeDataOpSamples - 1], snakeDataOp['col'][snakeDataOpSamples - 1]] snakeNeckData = [snakeDataOp['row'][snakeDataOpSamples - 2], snakeDataOp['col'][snakeDataOpSamples - 2]] # 计算小蛇方向 snakeDiraction = diraction(snakeHeadData, snakeNeckData) # 根据小蛇当前方向与键盘按键确定新方向 if snakeDiraction == 1 or snakeDiraction == 3: if key == 'left': controlConsOp.par.value1 = 2 elif key == 'right': controlConsOp.par.value1 = 0 elif snakeDiraction == 0 or snakeDiraction == 2: if key == 'up': controlConsOp.par.value1 = 1 elif key == 'down': controlConsOp.par.value1 = 3 # 游戏待机状态 elif gameState == 0: # 按下按键开始游戏 if state == 1: if key == 'up': controlConsOp.par.value1 = 1 elif key == 'down': controlConsOp.par.value1 = 3 controlConsOp.par.value0 = 1 return
到此,贪吃蛇游戏主体已经制作得差不多了。
- 结语 -
公众号后台回复”Snake“获取TD文件下载地址。
都看到这里了,不点个在看支持一下这个快要秃顶的小编吗?
原创不易
你们的关注是我们的最大动力
Follow
关 注 我 们
#
学习交流分享,请多多指教
Touchdesigner · Notch · Arduino · Blender